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.
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: trueMaven 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 querySample trace output (screenshot):
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-apiAccess the UI at http://localhost:8080 to view trace graphs (screenshot):
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.
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.
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.
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.
