You hit Enter. Your server did nothing
You type https://example.com/products/123 and press Enter. The mental model most engineers carry is browser → backend. Not even close. Your request may pass through five or more layers of infrastructure before your application code runs — and here is the part that surprises people: it can stop at almost every one of them. If it does, your backend never learns you asked.
This is not a corner case. On a well-tuned system, the majority of requests never touch the origin. Understanding where a request can terminate — and what each layer actually decides — is the difference between debugging in minutes and debugging for hours.
The actual journey
The naive picture — You → Browser → OS → DNS → CDN → Load Balancer → Server — is a useful mental model, but it describes only the cold, cache-miss path. The accurate picture is conditional: every layer first asks “can I answer this myself?”
Stop 1: browser cache
You request example.com/logo.png. Before anything touches the network, the browser checks its own cache: do I already have a fresh copy of this? If the previous response came with Cache-Control: max-age=86400 and it is still fresh, the browser returns it from disk or memory.
- 🚫 No DNS request
- 🚫 No TCP or TLS setup
- 🚫 No CDN, no load balancer, no backend
The network panel shows 200 OK (from disk cache) and zero milliseconds of network time. One important distinction: this applies to cacheable resources — static assets, and API responses explicitly marked cacheable — not blindly to every request. Anything sensitive should carry no-store, precisely so it cannot take this shortcut.
Stop 2: OS / DNS cache
Suppose the browser does need the network. First question: where is api.example.com? That name must become an IP address, and that lookup has its own cascade of caches: browser DNS cache → OS resolver cache → the recursive resolver (your ISP or 8.8.8.8) → root / TLD / authoritative servers.
Browser DNS cache (seconds to minutes)
↓ miss
OS resolver cache (stub resolver, honours TTL)
↓ miss
Recursive resolver (ISP / 8.8.8.8 — the big cache)
↓ miss
Root → TLD → Authoritative (the full lookup, rarely reached)Shortcut: connection reuse
Now the browser knows where to connect — but it may not need to connect at all. If an existing connection to that origin is still warm, it simply reuses it.
| Path | What happens before your bytes move |
|---|---|
| Cold connection | TCP handshake (1 RTT) → TLS handshake (1–2 RTT) → HTTP request |
| Warm HTTP/1.1 keep-alive | HTTP request (but only one at a time per connection) |
| Warm HTTP/2 connection | New stream on the existing multiplexed connection — zero setup |
| HTTP/3 0-RTT resumption | Request rides inside the very first QUIC packet |
The mechanics differ by protocol version and connection state, but the idea is constant: the browser works very hard not to pay connection setup twice. On a warm connection, the 2–3 round trips of setup collapse to zero.
Stop 3: the CDN plot twist
The request finally reaches the network. Your backend still might not see it — because there is a CDN with points of presence around the world, terminating requests a few milliseconds from the user.
GET /images/product-123.jpg
┌── Cache HIT → serve from edge → user (origin: silent)
Request → CDN ───┤
└── Cache MISS → fetch from origin, cache, serveOn a hit, the whole exchange is User → CDN → cached response → User. Your backend's logs show nothing; its metrics show nothing; from its perspective the request never existed. This is exactly why CDNs cut origin traffic by 80–95% on asset-heavy sites — and why "the site is up but users see stale data" bugs are so confusing until you remember this layer exists.
WAF, load balancer, and beyond
On a cache miss the request continues inward, and there can be several more gates before application code:
- WAF — inspects the request for SQLi, XSS and bot patterns. A blocked request dies here with a 403; your backend never sees the attack.
- Load balancer — picks a healthy instance (round robin, least connections) and can return a 503 itself when nothing healthy exists.
- API gateway — auth verification, rate limiting, routing. An expired token is rejected here with a 401 without touching your service.
Only after all of this does your application server process the request — possibly for the first time any of your code has run.
One request, many endings
| Layer | Early exit | What your backend sees |
|---|---|---|
| Browser cache | Fresh cached copy → response | Nothing |
| DNS caches | Lookup short-circuited (request continues) | Nothing (normal) |
| Connection reuse | Handshake skipped (request continues) | Nothing (normal) |
| CDN cache hit | Edge serves the response | Nothing |
| WAF | Malicious request blocked (403) | Nothing |
| Load balancer | No healthy targets (503) | Nothing |
| API gateway | Auth/rate-limit rejection (401/429) | Nothing |
| Backend | — | Finally, your code runs |
200 OK, it does not necessarily mean “my backend generated this response.” It means some layer answered. Knowing which layer answered is often the entire debugging problem.The debugging takeaway
When latency or a weird response appears, do not open your backend logs first. Walk the chain from the outside in:
- Browser cache? — check
(from disk cache)and the response'sAgeheader. - DNS? — slow
time_namelookupin curl, or a stale IP after a failover. - Connection? — high
time_connect/time_appconnectmeans setup, not app, is slow. - CDN? —
X-Cache: Hit/Miss,CF-Cache-Status, or theViaheader tell you who served it. - WAF / gateway? — a 403 or 429 with no application log line is the signature.
- Load balancer? — its own metrics show 5xx your app never emitted.
- Backend — only now do your logs become the source of truth.
Because sometimes the slow request never reached your application at all — and no amount of query optimisation will find it there.
Interview framing
“Between the browser and my service there are multiple layers where a request can terminate: browser cache, DNS caching, connection reuse, the CDN, a WAF, the load balancer and the API gateway. I design with that in mind — cacheable responses get explicit Cache-Control, personalized data gets no-store, and every layer adds an identifying header like X-Cache or a request ID so I can tell who answered. When debugging, I walk the chain from the edge inward instead of assuming my backend saw the request.”