Why Is My API Slow? Diagnose and Verify the Cause
Trace network, application, database, and dependency timing with a runnable Node.js example, then verify the fix under the same workload.
A slow API can spend time in the network, application, database, external services, or queues. Measure those stages before choosing a fix. This guide diagnoses a slow request; use the bottleneck workflow when performance deteriorates as concurrency increases.
1. Separate network time from application time
Prerequisites: an endpoint you may test, repeatable input, and application logs or traces. Start with a read-only staging request. Keep region, payload, credentials, and cache state consistent. Replace the example hostname below with your endpoint.
curl --silent --show-error --output /dev/null \
--write-out 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} first_byte=%{time_starttransfer} total=%{time_total}\n' \
https://api.example.com/healthThese values are seconds and cumulative milestones, not independent durations to add together. On a fresh HTTPS connection, subtract connect from TLS to estimate TLS setup. First-byte time includes networking and server work. Total minus first-byte time helps identify transfer cost. Redirects and connection reuse complicate interpretation. Repeat from the affected region and near the application. See the curl timing reference.
If the nearby test is fast but remote clients are slow, investigate DNS, geography, connection setup, packet loss, and payload size. Low application duration cannot rule out gateway queues.
2. Measure application timing
Save this runnable Node.js example as server.mjs and run node server.mjs. GET http://localhost:3001/health returns JSON and a Server-Timing header. The delay is illustrative, not a benchmark.
import { createServer } from 'node:http';
import { performance } from 'node:perf_hooks';
import { setTimeout as delay } from 'node:timers/promises';
createServer(async (req, res) => {
if (req.url !== '/health') {
res.writeHead(404).end();
return;
}
const start = performance.now();
await delay(20); // Replace with a measured application operation.
const body = JSON.stringify({ ok: true });
const appMs = performance.now() - start;
res.writeHead(200, {
'Content-Type': 'application/json',
'Server-Timing': 'app;dur=' + appMs.toFixed(1),
});
res.end(body);
}).listen(3001, '127.0.0.1');Expected result: HTTP 200, {"ok":true}, and an app duration around or above the delay. This timer excludes connection setup and final transmission. Measure database and dependency spans separately in a real handler. Correlate traces with request IDs; do not log authorization headers or request bodies. See Node performance timing.
3. Inspect database queries and pool waits
Record query count, query duration, and connection-pool acquisition time separately. Many fast queries can form an N+1 problem. Slow pool acquisition can resemble slow SQL execution.
Review execution plans and representative data before adding an index. Check rows scanned, lock waits, and filter or join indexes. Some execution-analysis tools actually execute statements; use a suitable test environment. Retest both reads and writes after an index change.
4. Time external services
Measure upstream duration, retries, and the total deadline. Retries can multiply a modest upstream delay. Apply bounded timeouts and retry only operations safe to retry. Parallelize independent calls only when failure handling and rate limits permit it.
Do not return fabricated success when an upstream request fails. Faster errors are not improved successful-request latency.
5. Check concurrency and resource saturation
A fast isolated request may queue behind other requests. Compare one worker with controlled increasing concurrency. Inspect CPU, event-loop delay, memory, garbage collection, database pool occupancy, and upstream quotas in the same time window.
If throughput flattens while p95 and queue depth rise, investigate the constrained resource using the baseline → load → investigate → retest workflow.
6. Verify the improvement
Change one likely cause, then repeat the same request mix, region, cache warm-up, duration, and concurrency. Compare response correctness, error rate, throughput, and p50/p95/p99. Repeat runs to distinguish improvements from normal variation.
Record version, hypothesis, change, sample count, before/after distributions, and errors. Include timeouts instead of silently dropping them. Reject a change that reduces latency by returning incomplete data or rejecting more requests.
Common diagnostic mistakes
- Comparing a warm cache after the fix with a cold cache before it.
- Timing SQL while ignoring pool waits.
- Treating client elapsed time as server CPU time.
- Generalizing one small response to every route.
Start with the online API testing tool to reproduce the request and inspect its response. Check pricing before saving requests or increasing load.