A jump from 300ms to 8 seconds is usually a traffic jam somewhere in the request path, not a single slow line of Java.
Question: API response increased from 300ms to 8 seconds. How do you debug?
Answer: I debug this by first proving where the time is spent: client, network, application, database, or a downstream service. I compare p50, p95, and p99 latency, inspect traces and logs for one slow request, and check saturation signals like CPU, GC, thread pools, and connection pools. Once I find the slow hop, I compare before and after the change to see whether it is a recent deploy, a config change, a dependency slowdown, or queueing under load.
Interview-Ready Answer: I start by narrowing the problem: is the 8 seconds on every request, only some requests, or only under load? Then I use metrics, logs, and distributed tracing to break the request into steps and find the slowest hop, often a database lock, connection pool exhaustion, or a retry loop. If this started after a deploy, I compare before and after, check slow query logs and thread dumps, and rollback or fix the bottleneck once I have evidence.
A latency spike means the request is waiting somewhere. In Java services, the most common waits are blocked threads, a slow database query, a full connection pool, garbage collection pauses, or a downstream API that is timing out and being retried. The key idea is simple: do not guess from one symptom; break the request into smaller pieces until one piece explains most of the 8 seconds.
| Signal | What it suggests | First thing to check |
|---|---|---|
| High p95, normal average | Tail latency | Slow dependency or queueing |
| Low CPU, high latency | Waiting | Locks, pools, network |
| High GC pause time | Memory pressure | Heap, allocation rate |
| One instance only | Node-specific issue | Bad host, config drift |
| All instances slow | Shared dependency | DB, cache, downstream API |
System.nanoTime() for elapsed time because it is monotonic, meaning it is not affected by clock changes. currentTimeMillis() is fine for timestamps, but not for precise duration measurements.| Tool | Best question | Example clue |
|---|---|---|
| Logs | What happened? | DB timeout after 7.8s |
| Metrics | How bad is it? | p95 jumped, CPU stable |
| Traces | Where did time go? | One span took 7.6s |
| Thread dump | What are threads doing? | Blocked on pool wait |
Think of the request as a highway: do not stare at one car, find the lane with the traffic jam. The job is to locate the blocked lane, not to guess which driver is late.
Real-World Story: In an e-commerce checkout service, a new pricing lookup was added so the API could show live discounts. The code worked in staging, but in production the pricing service occasionally slowed down to several seconds. Because the call had a long timeout and retries, checkout threads piled up, the Tomcat request queue grew, and users saw spinning loaders while trying to pay. The logs showed Read timed out messages, and the trace made it obvious that almost all of the 8 seconds sat inside one downstream span. The fix was to add a short timeout, a circuit breaker, and a fallback price path so checkout stayed fast even when pricing was unhealthy.
What goes wrong when this is misunderstood: Teams often blame the controller or add more servers too early. That can hide the real issue for a while, but the symptoms keep showing up as rising thread counts, connection pool exhaustion, and user complaints like slow pages, failed logins, or abandoned carts. The outage is not just slower code; it is queued work building up faster than the system can drain it.
import java.util.ArrayList;
import java.util.List;
public class ApiLatencyDebugDemo {
// Anything above this is worth a closer look in a real service.
private static final long SLOW_STEP_THRESHOLD_MS = 200;
public static void main(String[] args) {
runScenario("Baseline", 25, 35, 55, 45, false);
System.out.println();
runScenario("Regression", 25, 35, 900, 45, false);
System.out.println();
runScenario("Failure path", 25, 35, 300, 45, true);
}
private static void runScenario(String name, int authMs, int cacheMs, int dbMs, int downstreamMs, boolean failDb) {
System.out.println("== " + name + " ==");
List<StepResult> results = new ArrayList<StepResult>();
long requestStart = System.nanoTime();
try {
// Each step is timed separately so we can see where the request waited.
results.add(runStep("auth", authMs, false));
results.add(runStep("cache", cacheMs, false));
StepResult db = runStep("db", dbMs, failDb);
results.add(db);
if (!db.success) {
// We stop here because a failed dependency is often the real latency root cause.
throw new RuntimeException(db.errorMessage);
}
results.add(runStep("downstream", downstreamMs, false));
System.out.println("total=" + elapsedMs(requestStart) + "ms");
printAnalysis(results);
} catch (Exception e) {
System.out.println("request failed after " + elapsedMs(requestStart) + "ms");
System.out.println("error=" + e.getClass().getSimpleName() + ": " + e.getMessage());
printAnalysis(results);
System.out.println("next step: inspect slow logs, thread dumps, and the last completed span.");
}
}
private static StepResult runStep(String name, int simulatedMs, boolean fail) {
long start = System.nanoTime();
try {
simulateWork(simulatedMs);
if (fail) {
// A dependency can be slow and then fail; that still consumes the user's time budget.
throw new IllegalStateException("DB lock wait timeout");
}
return new StepResult(name, elapsedMs(start), true, null);
} catch (Exception ex) {
if (ex instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
return new StepResult(name, elapsedMs(start), false, ex.getMessage());
}
}
private static void simulateWork(int ms) throws InterruptedException {
if (ms > 0) {
Thread.sleep(ms);
}
}
private static long elapsedMs(long startNanos) {
return (System.nanoTime() - startNanos) / 1_000_000L;
}
private static void printAnalysis(List<StepResult> results) {
if (results.isEmpty()) {
System.out.println("no spans captured");
return;
}
StepResult slowest = results.get(0);
for (StepResult r : results) {
String status = r.success ? "ok" : "failed";
System.out.println(r.name + "=" + r.durationMs + "ms (" + status + ")");
if (r.durationMs > slowest.durationMs) {
slowest = r;
}
}
if (slowest.durationMs >= SLOW_STEP_THRESHOLD_MS) {
System.out.println("slowest=" + slowest.name + " took " + slowest.durationMs + "ms");
}
for (StepResult r : results) {
if (!r.success) {
System.out.println("suspect=" + r.name + " because it failed after waiting");
return;
}
}
}
private static final class StepResult {
final String name;
final long durationMs;
final boolean success;
final String errorMessage;
StepResult(String name, long durationMs, boolean success, String errorMessage) {
this.name = name;
this.durationMs = durationMs;
this.success = success;
this.errorMessage = errorMessage;
}
}
}
Follow-up & Tricky Questions:
Tricky / gotcha questions:
Common Mistakes:
Memory Hook: Traffic jam first, engine second. When latency explodes, look for the blocked lane in the request path before you inspect the code that drove into it.
Cheat Sheet:
Practice Tasks:
System.nanoTime() and print its duration.