6 Proven Ways to Measure API Latency in Spring Boot 3

This article presents eight practical techniques—including manual StopWatch, custom AOP, interceptor, filter, event listener, Micrometer + Prometheus, Arthas, and SkyWalking—to accurately record and monitor Spring Boot API request times, comparing their intrusiveness, scope, and suitability for different scenarios.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
6 Proven Ways to Measure API Latency in Spring Boot 3

Introduction

In micro‑service architectures and high‑concurrency environments, API response time directly impacts user experience and system stability. Traditional manual timing (e.g., inserting start/end timestamps) is intrusive and hard to maintain at scale. This article introduces eight practical ways to record API request latency in Spring Boot 3, helping developers choose the most appropriate monitoring approach.

Practical Solutions

2.1 Manual Recording

Use Spring's StopWatch to measure method execution time.

@GetMapping("/query")
public ResponseEntity<?> query() throws Exception {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // business logic
    TimeUnit.MILLISECONDS.sleep(new Random().nextLong(2000));
    stopWatch.stop();
    System.out.printf("Method took: %dms%n", stopWatch.getTotalTimeMillis());
    return ResponseEntity.ok("api query...");
}

Result example: Method took: 1096ms Drawbacks: strong code intrusion and repeated boiler‑plate code violate the DRY principle.

2.2 Custom AOP Recording

Define an aspect that intercepts controller annotations to log execution time without modifying business code.

@Aspect
@Component
public class PerformanceAspect {
    private static final Logger logger = LoggerFactory.getLogger("api.timed");

    @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.GetMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PutMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.DeleteMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PatchMapping)")
    public Object recordExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch sw = new StopWatch();
        sw.start();
        Object result = pjp.proceed();
        sw.stop();
        logger.info("Method [{}] took: {}ms", pjp.getSignature(), sw.getTotalTimeMillis());
        return result;
    }
}

Sample log: [method] com.example.ApiController.query() - 487ms Pros: non‑intrusive, unified management, suitable for global monitoring. Cons: ineffective for non‑Spring‑managed methods, adds AOP overhead.

2.3 Interceptor Technique

Implement HandlerInterceptor to record start time before handling and compute duration after completion.

public class TimedInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        request.setAttribute("startTime", System.currentTimeMillis());
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        long startTime = (Long) request.getAttribute("startTime");
        long cost = System.currentTimeMillis() - startTime;
        System.out.printf("Request [%s] took: %dms%n", request.getRequestURI(), cost);
    }
}
@Component
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TimedInterceptor()).addPathPatterns("/api/**");
    }
}

Result example: Request [/api/query] took: 47ms Pros: centralized, low intrusion for controller layer. Cons: only measures HTTP request handling, cannot capture internal method latency.

2.4 Filter Technique

Use a servlet Filter to time the whole request processing.

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestTimingFilter implements Filter {
    @Value("${timing.filter.exclude-paths}")
    private String[] excludePaths;
    private List<PathPattern> excludedPatterns = Collections.emptyList();
    private static final Logger logger = LoggerFactory.getLogger(RequestTimingFilter.class);

    @Override
    public void init(FilterConfig filterConfig) {
        excludedPatterns = Arrays.stream(excludePaths)
                .map(path -> new PathPatternParser().parse(path))
                .toList();
        logger.info("Excluding URIs: {}", Arrays.toString(excludePaths));
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String uri = httpRequest.getRequestURI();
        if (shouldExclude(uri)) { chain.doFilter(request, response); return; }
        long start = System.nanoTime();
        try { chain.doFilter(request, response); }
        finally {
            long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
            if (response instanceof HttpServletResponse httpResponse) {
                logger.info("[{}] {} - {}ms (Status: {})", httpRequest.getMethod(), uri, duration, httpResponse.getStatus());
            } else {
                logger.info("[{}] {} - {}ms", httpRequest.getMethod(), uri, duration);
            }
        }
    }

    private boolean shouldExclude(String uri) {
        return excludedPatterns.stream().anyMatch(p -> p.matches(PathContainer.parsePath(uri)));
    }
}

Sample log: [GET] /api/query - 379ms (Status: 200) Pros: non‑intrusive, suitable for global request monitoring. Cons: coarse granularity; cannot pinpoint specific method latency.

2.5 Event Listener

Listen to ServletRequestHandledEvent emitted after request completion.

@Component
public class TimedListener {
    @EventListener(ServletRequestHandledEvent.class)
    public void recordTimed(ServletRequestHandledEvent event) {
        System.err.println(event);
    }
}

Sample output includes URL, method, status, and total time (e.g., time=[696ms]).

Pros: zero code changes, global timing. Cons: only provides overall request duration, not method‑level details.

2.6 Micrometer + Prometheus

Annotate methods with @Timed and expose metrics via Spring Boot Actuator for Prometheus scraping.

@Timed(value = "api.query", description = "Query business API")
@GetMapping("/query")
public ResponseEntity<?> query() throws Exception {
    TimeUnit.MILLISECONDS.sleep(new Random().nextLong(2000));
    return ResponseEntity.ok("api query...");
}

Configuration snippets (application.yml):

management:
  observations:
    annotations:
      enabled: true

management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    prometheus:
      enabled: true

Maven dependencies:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Prometheus scrape config example:

- job_name: "testtag"
  metrics_path: "/ac/prometheus"
  static_configs:
    - targets: ["localhost:8080"]

Resulting Grafana/Prometheus dashboards visualize method‑level latency.

Pros: fine‑grained, integrates with Spring ecosystem, data persistence and visualization. Cons: adds monitoring dependencies and configuration complexity.

2.7 Arthas

Arthas is an online diagnostic tool that can trace method execution time without code changes.

Download from GitHub and start: java -jar arthas-boot.jar After attaching to the target JVM, run:

trace com.pack.timed.controller.ApiController query

Sample trace output (screenshot):

Arthas trace result
Arthas trace result

Pros: non‑intrusive, works on running JVMs, provides detailed method arguments and timing. Cons: requires JVM attachment and may have performance overhead during tracing.

2.8 SkyWalking

SkyWalking is an open‑source APM that uses Java agents to automatically trace distributed calls and record latency.

Download the appropriate agent from SkyWalking downloads , start the OAP server, then launch the application with:

-javaagent:/path/to/skywalking-agent.jar
-Dskywalking.agent.service_name=pack-api

Access the UI at http://localhost:8080 to view trace graphs (screenshot):

SkyWalking UI
SkyWalking UI

Pros: zero code changes, supports distributed tracing, rich visualization. Cons: agent setup required and may add slight runtime overhead.

Conclusion

The article compares each technique’s intrusiveness, scope, and suitability, helping developers select the best method for their Spring Boot performance monitoring needs.

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.

performanceSkyWalkingAPI monitoringmicrometerspring-boot
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

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.