Why is my API slow locally?
A slow API on localhost is almost never the network — it’s usually one of five things: IPv6 DNS resolution of localhost, connections not being reused, dev-mode framework overhead, cold caches, or a tiny local database pool. The fix is different for each, so the first step is finding out which phase of the request is eating the time.
1. localhost vs 127.0.0.1 (the IPv6 trap)
localhost resolves to both ::1 (IPv6) and 127.0.0.1 (IPv4). If your server only listens on IPv4, some clients try IPv6 first, wait for it to fail, then fall back — adding anywhere from a few milliseconds to multiple seconds per request. The tell: requests to 127.0.0.1:3000 are instant while localhost:3000 lags. Use the IP directly, or bind your server to both stacks.
2. No connection reuse
Without HTTP keep-alive, every request pays a fresh TCP handshake — and a TLS handshake too if you’re running HTTPS locally. On loopback that’s small per request, but mobile emulators, test runners, and some HTTP clients (default Node fetch agents, older Python requests usage) open a new connection every time. Symptom: TCP/TLS time is nonzero on every request instead of just the first.
3. Dev mode isn’t production mode
Frameworks trade speed for feedback in development: recompile-on-request (Next.js, Vite dev SSR), autoreload (Django, Rails), unminified assets, no response caching. A first hit after a code change can cost seconds of compile time that says nothing about production. Measure the second and third request to the same route before concluding anything.
4. Cold caches and lazy connections
Local databases start with empty page caches, ORMs open connections lazily, and serverless emulators cold-start functions. The signature is a fast/slow split: the first request to a route is 10× slower than the rest. That pattern has a name — see bimodal latency below — and it’s worth distinguishing from a route that is always slow.
5. The query was always slow — production just hides it
Production has read replicas, warmed caches, and a CDN between users and your slowest query. Localhost has none of that, so an unindexed query or an N+1 pattern shows its true cost. This is the one case where “slow locally” is a gift: fix it now and production gets faster too.
How to tell which one you have
Split a request into its phases — DNS, TCP connect, TLS, time to first byte (TTFB), download — and the cause reads off directly:
- DNS slow → the localhost/IPv6 trap (#1)
- TCP/TLS nonzero on every request → no keep-alive (#2)
- TTFB huge on first hit, fine after → dev compile or cold cache (#3, #4)
- TTFB consistently high → server-side work: query, N+1, blocking I/O (#5)
You can get these phases from curl -w for a single request, or run your app’s real traffic through a local profiler like Lobe (lobe capture http://localhost:3000) to see the phase breakdown for every request as you click around:
curl -w "dns %{time_namelookup}s connect %{time_connect}s \
tls %{time_appconnect}s ttfb %{time_starttransfer}s total %{time_total}s\n" \
-o /dev/null -s http://localhost:3000/api/users