Why Is My Website Slow? — Diagnosing Latency Layer by Layer
"The site feels slow" is one of the least actionable bug reports in existence — and one of the most common. Slow where? Slow resolving your domain, slow shaking hands with TLS, slow generating the page, slow delivering it, or slow rendering three megabytes of JavaScript after delivery? Each of those is a different problem owned by a different layer with a different fix — and guessing wrong means a week optimizing images when the real culprit was an N+1 query. Here's the layer-by-layer method, with the one command that splits the problem in five.
The one-liner that does the triage
curl will happily time every phase of a request for you:
curl -o /dev/null -s -w "dns: %{time_namelookup}s
connect: %{time_connect}s
tls: %{time_appconnect}s
ttfb: %{time_starttransfer}s
total: %{time_total}s
" https://example.com/
The output might look like:
dns: 0.062s
connect: 0.097s
tls: 0.184s
ttfb: 1.437s
total: 1.561s
These numbers are cumulative — each includes everything before it — so read the gaps:
dns— time to resolve the hostname to an IP.connect−dns— the TCP handshake: pure network round-trip to your server.tls−connect— the TLS handshake on top.ttfb−tls— time to first byte of the response: your server thinking. This is the application and database signal.total−ttfb— downloading the response body.
In the example above, the verdict is instant: DNS, TCP and TLS are all healthy, and 1.25 of the 1.56 seconds is TTFB — this is a back-end problem, and no amount of image compression will touch it. Run the command a few times (the first run pays DNS and connection costs that later ones may cache) and you have a defensible diagnosis in under a minute.
Layer 1: DNS resolution
A slow or flaky DNS provider taxes every first visit, before your server is even involved. Healthy resolution from a warm resolver is typically tens of milliseconds; if you're seeing hundreds consistently, look at:
- A slow authoritative provider — the budget DNS hosting bundled with a cheap registrar is a common offender. Anycast DNS providers answer from a location near the user instead of one server in one country.
- Too-clever setups — long CNAME chains each add a lookup; a very low TTL (like 60 seconds) means resolvers can barely cache you, so more users pay the full lookup price.
Layer 2: TCP and TLS handshakes
The connect gap is physics: a round trip between the probe and your server. There's no software fix for distance — if your users are in Europe and the server is in Oregon, that's ~140 ms before a single byte of HTTP, on every new connection. Fixes are placement fixes: host near your users, or put a CDN in front so the handshake happens at an edge near them.
The TLS gap adds one to two more round trips on top. Worth checking here:
- Keep-alive actually enabled. If connections aren't reused, every request re-pays TCP + TLS. It's on by default in sane configs — and turned off by surprising numbers of legacy snippets (
keepalive_timeout 0;in nginx is the classic). - HTTP/2 enabled, so one connection multiplexes many requests instead of paying handshakes in parallel.
- Session resumption (and OCSP stapling where your CA still supports it) — small wins that trim repeat-visit handshakes and avoid a side-trip to the CA.
Layer 3: TTFB — the application and database signal
This is the layer where most "the site got slow over time" stories live. TTFB is everything your stack does between receiving the request and emitting the first byte: routing, application code, database queries, template rendering, external API calls. When it's high, the usual suspects, in rough order of likelihood:
- N+1 queries. The page loads 50 products with one query, then runs one more query per product for images or prices — 51 round-trips where 2 would do. Invisible in development with ten rows; devastating in production with real data. Every ORM has an eager-loading fix; the hard part is noticing.
- Missing indexes. The query that was instant at ten thousand rows table-scans at ten million. If TTFB has drifted up as data grew, this is the first place to look —
EXPLAINis your friend. - No caching where it's cheap. Recomputing an expensive homepage on every request, refetching config from the database per request, calling a third-party API synchronously in the request path.
- Cold starts and exhausted worker pools — the request isn't slow, the queueing before it is. Watch for TTFB that's fine at 3 AM and terrible at peak.
Layer 4: Download and front-end weight
If total − ttfb is large, you're shipping too many bytes or shipping them badly: unoptimized images (multi-megabyte hero PNGs that should be compressed and properly sized), missing gzip/brotli compression on text assets, no CDN for static files. And beyond what curl can see, there's the front-end layer — render-blocking scripts, heavy JavaScript bundles, fonts loaded from three origins. curl measures the document; the browser's DevTools waterfall and a Lighthouse run measure the experience. If curl says everything is fast but users say the site is slow, the problem lives in the browser, not the server.
Measure from outside — and over time
Two habits turn one-off diagnosis into an early-warning system:
- Measure from outside your own network. From your office, DNS is cached, the route is short, and your CDN edge is warm — you're testing the best case. Your users get cold caches, distant geography and mobile networks. Checks from external locations (plural — one region's numbers won't represent another's) reflect what users actually experience, and catch the regional degradations you'd never see from your desk.
- Trend response time continuously. Slowness rarely arrives as an event; it arrives as drift — 300 ms in autumn, 500 by December, 900 by February, each week imperceptibly worse than the last. Nobody notices drift without a baseline. A monitor that records response time on every check draws the trend line for you, and a degradation alert ("sustained response time above threshold") catches the slide months before it becomes the outage where the database finally tips over.
From "feels slow" to a graph and a culprit
The layer-by-layer method turns a vague complaint into a specific, fixable finding — and continuous measurement means the next regression announces itself instead of accumulating. CompleteStatus records response time on every check and charts the trend over weeks and months — so you can see exactly when the drift started (usually suspiciously close to a deploy) and alert on degradation long before hard downtime. And because a fast response isn't automatically a correct one, the same monitors support content assertions too — we covered that failure class in The page loads, but it's broken.
Create a free account, point a monitor at your slowest page, and let the baseline start building today — when someone next says "the site feels slow," you'll answer with a graph and a layer instead of a shrug.