6 Proven Ways to Accurately Measure API Latency in Java Applications
This article explains why measuring API response time is crucial for performance optimization, monitoring, and user experience, and presents six practical methods—from simple System.currentTimeMillis() calls to Spring AOP, interceptors, filters, and production‑grade Micrometer/APM tools—complete with code examples, pros, cons, and suitable scenarios.
Introduction
In real projects, API latency often goes unnoticed until users complain about slow responses, making it essential to master accurate latency measurement.
Why measuring API latency is important
From an architect's perspective, latency reflects system performance and serves as:
Performance optimization foundation : without timing data, optimization is blind.
Source of monitoring alerts : latency trends reveal anomalies such as slow SQL or resource contention.
User experience indicator : even a few milliseconds of delay can cause user churn under high concurrency.
Simply logging System.currentTimeMillis() at method start and end may be inaccurate in multithreaded environments due to clock adjustments or logging overhead.
Method 1: System.currentTimeMillis()
Why use this method?
Quick and effective for simple scenarios like testing a code block; requires no third‑party libraries.
Example code
public class SimpleTimeTracker {
public void processRequest() {
long startTime = System.currentTimeMillis(); // record start
try {
Thread.sleep(100); // simulate business logic
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis(); // record end
long duration = endTime - startTime; // calculate latency
System.out.println("接口耗时: " + duration + "ms");
}
public static void main(String[] args) {
new SimpleTimeTracker().processRequest();
}
}Code logic details
System.currentTimeMillis()returns the number of milliseconds since 1970‑01‑01 UTC; call cost is low.
Save the value at method entry as startTime.
Save the value at method exit as endTime.
Latency = endTime - startTime (milliseconds).
Print or log the result.
Deep analysis
Precision issue : millisecond precision may be insufficient for very short operations; System.nanoTime() offers nanosecond precision but measures relative time only.
System time adjustments : if the system clock is changed (e.g., NTP sync), currentTimeMillis can jump or become negative; nanoTime is immune because it is based on JVM start time.
Code intrusiveness : manual insertion in many methods can become cumbersome and error‑prone.
Applicable scenarios: quick debugging, simple single‑threaded apps, learning the basic concept.
Method 2: System.nanoTime()
Why use this method?
Provides nanosecond‑level precision, ideal for high‑performance or low‑latency contexts where millisecond granularity is not enough.
Example code
public class NanoTimeTracker {
public void processRequest() {
long startTime = System.nanoTime(); // nanosecond start
try {
Thread.sleep(100); // simulate work (milliseconds)
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.nanoTime(); // nanosecond end
long duration = (endTime - startTime) / 1_000_000; // convert to ms
System.out.println("接口耗时: " + duration + "ms");
}
public static void main(String[] args) {
new NanoTimeTracker().processRequest();
}
}Code logic details
System.nanoTime()returns a nanosecond‑level timestamp suitable only for measuring elapsed time.
Convert the nanosecond difference to milliseconds for readability.
Note that Thread.sleep(100) uses milliseconds; real business logic may be truly nanosecond‑level.
Deep analysis
Different purpose : currentTimeMillis gives wall‑clock time, while nanoTime measures relative intervals.
Precision and performance : nanoTime is more precise; overhead is negligible on modern JVMs.
Overflow : the value can overflow only after ~292 years, which is practically impossible.
Recommendation: use nanoTime when high precision is required; otherwise currentTimeMillis suffices.
Method 3: Spring AOP
Why use this method?
Separates cross‑cutting concerns such as logging and latency measurement from business logic, keeping code clean and maintainable.
Example code
// Annotation definition
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeCost { String value() default ""; }
// Aspect class
@Aspect
@Component
public class TimeCostAspect {
private static final Logger logger = LoggerFactory.getLogger(TimeCostAspect.class);
@Around("@annotation(timeCost)")
public Object around(ProceedingJoinPoint joinPoint, TimeCost timeCost) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = null;
try {
result = joinPoint.proceed();
} finally {
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
logger.info("方法 {} 耗时: {}ms", joinPoint.getSignature().getName(), duration);
}
return result;
}
}
// Business service
@Service
public class UserService {
@TimeCost("获取用户信息")
public User getUserById(Long id) {
try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); }
return new User(id, "用户" + id);
}
}Code logic details
Define a custom annotation @TimeCost to mark methods that need timing.
The aspect intercepts annotated methods with @Around, records start and end times using System.currentTimeMillis(), and logs the duration.
Business methods remain unchanged, preserving clean code.
Deep analysis
AOP works only on Spring‑managed beans; private methods or non‑bean objects cannot be intercepted. Proxy creation adds a slight overhead, but it is usually negligible compared to the benefits of decoupling.
Method 4: Spring MVC Interceptor
Why use this method?
Interceptors operate at the web layer, allowing access to HTTP request details (parameters, URI, status) and are well‑suited for API‑level latency tracking.
Example code
@Component
public class TimeCostInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(TimeCostInterceptor.class);
private static final String START_TIME_ATTRIBUTE = "startTime";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
long startTime = System.currentTimeMillis();
request.setAttribute(START_TIME_ATTRIBUTE, startTime);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
long startTime = (Long) request.getAttribute(START_TIME_ATTRIBUTE);
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
logger.info("接口 {} 耗时: {}ms, 状态码: {}", request.getRequestURI(), duration, response.getStatus());
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TimeCostInterceptor timeCostInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timeCostInterceptor).addPathPatterns("/**");
}
}Code logic details
preHandlerecords the start time and stores it in the request attributes. afterCompletion runs after the controller and view rendering, retrieves the start time, computes the duration, and logs the URI, latency, and HTTP status.
Deep analysis
Interceptor measures the time of the controller method plus view rendering, which may be longer than pure business logic.
Only Spring MVC requests are intercepted; non‑web calls are unaffected.
Method 5: Servlet Filter
Why use this method?
Filters are part of the Servlet specification and execute before any servlet processing, making them suitable for measuring the full request lifecycle across any Java web container.
Example code
@Component
@Order(1)
public class TimeCostFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(TimeCostFilter.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
long startTime = System.currentTimeMillis();
try {
chain.doFilter(request, response);
} finally {
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
logger.info("过滤器统计 - 接口 {} 耗时: {}ms, 状态码: {}", httpRequest.getRequestURI(), duration, httpResponse.getStatus());
}
}
}Code logic details
Implement Filter and record start time before delegating to the next filter or servlet.
In the finally block, record end time, compute duration, and log URI, latency, and status. @Order(1) ensures this filter runs early in the chain.
Deep analysis
Because the filter sits at the outermost layer, the measured time includes all downstream filters, interceptors, controller execution, and view rendering. This provides a complete end‑to‑end latency figure but may be longer than business‑logic‑only timing.
Method 6: Micrometer and APM tools
Why use this method?
Production‑grade observability solutions offer low overhead, distributed tracing, aggregation, and alerting capabilities that go far beyond manual logging.
Example: Micrometer with Spring Boot Actuator
Add the following Maven dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>Enable the Prometheus endpoint in application.properties:
management.endpoints.web.exposure.include=prometheus,metricsCustom timer example
@Service
public class OrderService {
private final MeterRegistry meterRegistry;
private final Timer orderProcessTimer;
public OrderService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.orderProcessTimer = Timer.builder("order.process.time")
.description("订单处理耗时")
.register(meterRegistry);
}
public void processOrder(Order order) {
orderProcessTimer.record(() -> {
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
});
}
}Deep analysis
Micrometer provides a façade for many monitoring systems (Prometheus, Datadog, etc.). Metrics are exported via Actuator endpoints and can be scraped centrally.
APM tools such as SkyWalking or Pinpoint instrument bytecode at runtime, offering automatic distributed tracing without code changes.
Conclusion
The six methods span from simple manual timing to enterprise‑grade observability. Choose the technique that matches your environment, precision needs, and maintenance preferences. Remember that measuring latency is a means to deliver stable, fast services for end users.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
