6 Ways to Measure API Response Time in Java

This article examines six practical techniques for measuring the latency of online interfaces in Java, from simple System.currentTimeMillis() calls to advanced AOP, interceptors, filters, and production‑grade monitoring tools like Micrometer and APM, comparing their precision, intrusiveness, and suitable scenarios.

Programmer XiaoFu
Programmer XiaoFu
Programmer XiaoFu
6 Ways to Measure API Response Time in Java

Why measuring API latency matters

Accurate response‑time statistics are the foundation of performance optimization, monitoring alerts, and user‑experience assessment. Without reliable latency data, it is impossible to locate bottlenecks, set meaningful SLAs, or detect abnormal behavior such as slow SQL queries or resource contention.

Method 1: System.currentTimeMillis()

Why use it?

It is the most basic, native Java approach. Recording the current time at method entry and exit and subtracting the two values works well for quick, single‑threaded tests and for developers who need a zero‑dependency solution.

Example code

public class SimpleTimeTracker {
    public void processRequest() {
        long startTime = System.currentTimeMillis(); // record start
        // simulate work
        try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
        long endTime = System.currentTimeMillis(); // record end
        long duration = endTime - startTime; // calculate latency
        System.out.println("Interface latency: " + duration + "ms");
    }
    public static void main(String[] args) {
        new SimpleTimeTracker().processRequest();
    }
}

Deep analysis

Precision issue : millisecond granularity may be insufficient for very short operations; System.nanoTime() offers nanosecond precision.

System time adjustments : if the system clock is altered (e.g., NTP sync), currentTimeMillis can produce negative or erratic values, while nanoTime is immune because it measures elapsed time since JVM start.

Code intrusiveness : developers must manually insert timing code in every method, which becomes cumbersome and error‑prone for large codebases.

Applicable scenarios

Quick debugging or local testing.

Simple single‑threaded applications where high precision is not required.

Learning foundation before adopting more advanced techniques.

Method 2: System.nanoTime()

Why use it?

Provides nanosecond‑level precision and is unaffected by system‑time changes, making it suitable for high‑performance or low‑latency systems where millisecond granularity is too coarse.

Example code

public class NanoTimeTracker {
    public void processRequest() {
        long startTime = System.nanoTime(); // start in nanoseconds
        // simulate work
        try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
        long endTime = System.nanoTime(); // end in nanoseconds
        long duration = (endTime - startTime) / 1_000_000; // convert to ms
        System.out.println("Interface latency: " + duration + "ms");
    }
    public static void main(String[] args) {
        new NanoTimeTracker().processRequest();
    }
}

Deep analysis

Different purpose : currentTimeMillis returns wall‑clock time, while nanoTime is intended solely for measuring elapsed intervals.

Precision and performance : nanoTime is slightly more expensive but the overhead is negligible on modern JVMs.

Overflow : The value can overflow after ~292 years, but when used only for interval calculation this is never an issue.

Applicable scenarios

Performance‑critical code, algorithm benchmarking, low‑latency trading systems.

When sub‑millisecond accuracy is required.

Method 3: Spring AOP

Why use it?

Spring AOP enables non‑intrusive timing by applying an aspect to any Spring‑managed bean. Business logic stays clean while the aspect records start and end times.

Example code

// Annotation to mark methods
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeCost { String value() default ""; }

@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 start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        logger.info("Method {} took {} ms", joinPoint.getSignature().getName(), end - start);
        return result;
    }
}

@Service
public class UserService {
    @TimeCost("Get user info")
    public User getUserById(Long id) {
        try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); }
        return new User(id, "User" + id);
    }
}

Deep analysis

AOP works only on Spring‑managed beans; private methods or non‑Spring objects cannot be intercepted.

Spring creates proxies (JDK dynamic proxy or CGLIB) which add a small runtime overhead.

It cleanly separates cross‑cutting concerns, keeping business code free of timing boilerplate.

Applicable scenarios

Spring projects that need non‑intrusive timing of service‑layer methods.

When many methods share the same timing logic.

Method 4: Spring MVC Interceptor

Why use it?

Interceptors operate at the web‑request level, allowing access to HTTP request and response data. They are lighter than full AOP and are ideal for measuring the whole request processing time, including controller execution.

Example code

@Component
public class TimeCostInterceptor implements HandlerInterceptor {
    private static final Logger logger = LoggerFactory.getLogger(TimeCostInterceptor.class);
    private static final String START = "startTime";
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        request.setAttribute(START, System.currentTimeMillis());
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        long start = (Long) request.getAttribute(START);
        long duration = System.currentTimeMillis() - start;
        logger.info("Endpoint {} latency: {} ms, status: {}", request.getRequestURI(), duration, response.getStatus());
    }
}

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private TimeCostInterceptor interceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor).addPathPatterns("/**");
    }
}

Deep analysis

Interceptor runs before the controller (preHandle) and after view rendering (afterCompletion), so the measured time includes view rendering.

Only works for HTTP requests; it cannot time non‑web business methods.

Performance impact is minimal because it is part of the servlet filter chain.

Applicable scenarios

Spring MVC applications that need request‑level latency without modifying service code.

When HTTP‑specific data (URI, status code) is required for monitoring.

Method 5: Servlet Filter

Why use it?

Filters are the earliest hook in the servlet container, capable of measuring the complete lifecycle from request receipt to response commit, even for static resources. They do not depend on Spring.

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 start = System.currentTimeMillis();
        try { chain.doFilter(request, response); } finally {
            long duration = System.currentTimeMillis() - start;
            HttpServletRequest req = (HttpServletRequest) request;
            HttpServletResponse res = (HttpServletResponse) response;
            logger.info("Filter - Endpoint {} latency: {} ms, status: {}", req.getRequestURI(), duration, res.getStatus());
        }
    }
}

Deep analysis

Because it sits at the outermost layer, the measured time includes filter chain execution, interceptor processing, controller handling, and view rendering.

Useful for full‑stack latency monitoring, but the numbers are larger than AOP or interceptor measurements.

Works with any Java web container (Tomcat, Jetty, etc.).

Applicable scenarios

When you need an end‑to‑end request latency metric, regardless of framework.

Legacy servlet applications without Spring.

Method 6: Micrometer and APM tools

Why use it?

Production‑grade monitoring libraries such as Micrometer or APM solutions (SkyWalking, Pinpoint) provide low‑overhead, distributed tracing, aggregation, and alerting. They are designed for micro‑service environments.

Example: Micrometer with Spring Boot Actuator

# pom.xml 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>

# application.properties
management.endpoints.web.exposure.include=prometheus,metrics

Custom timer for a specific method:

@Service
public class OrderService {
    private final Timer orderTimer;
    public OrderService(MeterRegistry registry) {
        this.orderTimer = Timer.builder("order.process.time")
            .description("Order processing latency")
            .register(registry);
    }
    public void processOrder(Order order) {
        orderTimer.record(() -> {
            try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
        });
    }
}

Deep analysis

Micrometer automatically instruments HTTP requests (e.g., http.server.requests) and can export to Prometheus, Datadog, etc.

Custom timers let you record business‑method latency with a single line of code.

APM agents (SkyWalking, Pinpoint) add bytecode instrumentation at runtime, requiring no code changes, and provide full‑stack tracing across services.

Applicable scenarios

Small‑to‑medium projects: Micrometer + Prometheus + Grafana for cost‑effective monitoring.

Large distributed systems: APM tools for traceability and advanced analytics.

Summary

The six methods above cover the spectrum from simple, code‑intrusive techniques to sophisticated, production‑ready monitoring solutions. Choose the approach that matches your precision requirements, deployment environment, and maintenance preferences.

Key take‑aways:

Use System.currentTimeMillis() for quick, low‑precision checks.

Prefer System.nanoTime() when sub‑millisecond accuracy is needed.

Adopt Spring AOP for non‑intrusive service‑layer timing in Spring applications.

Select interceptors when you need HTTP‑level metrics without full request‑chain overhead.

Employ servlet filters for complete end‑to‑end latency measurement.

Leverage Micrometer or APM tools for production monitoring, distributed tracing, and alerting.

Remember that latency measurement is a means to an end: delivering stable, fast services to users.

Method flow diagram
Method flow diagram
AOP vs Interceptor vs Filter
AOP vs Interceptor vs Filter
Comparison table
Comparison table

Method comparison (extracted from the original table):

Method                | Advantages                         | Disadvantages                     | Suitable scenarios
----------------------|-----------------------------------|----------------------------------|-------------------
System.currentTimeMillis() | Simple, no dependencies          | Low precision, intrusive code    | Local testing, quick debugging
System.nanoTime()         | High precision                    | Intrusive, unit conversion       | High‑performance measurement
Spring AOP                | Non‑intrusive, decoupled           | Only Spring beans, proxy overhead| Business‑method monitoring in Spring
Interceptor               | Web‑optimized, access to HTTP data| Includes view rendering time    | Web API monitoring
Filter                    | Full‑chain, framework‑agnostic    | Measures all filter overhead      | End‑to‑end request latency
Micrometer/APM            | Production‑grade, distributed support| Configuration complexity, infra needed| Production, micro‑services
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaPerformanceAOPMetricsSpring
Programmer XiaoFu
Written by

Programmer XiaoFu

xiaofucode.com – a programmer learning guide driven by the pursuit of profit

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.