The life of a request
Every hop a request takes: anycast routing to the nearest location, TLS, DDoS and WAF filtering, the edge cache, the isolate, the database read, and the response — plus which hops you can observe and control.
This page follows one request from a user's browser to a response, naming every hop, what can stop it, and what you can observe about it. It's the mental model everything else in this section builds on.
The path
Two properties of this diagram deserve emphasis in an architecture review:
Filtering happens before execution. Traffic rejected by DDoS mitigation, the WAF, or your own firewall rules never reaches an isolate. Under attack, that is the difference between a cost spike and a non-event.
A cache hit is not a fast path through your app — it's the absence of your app. No isolate starts, no query runs, nothing is logged, and nothing is billed for compute. Deciding which routes are cacheable is therefore both a performance decision and a resilience one: cached routes keep serving when a database binding is degraded.
What runs where
| Hop | Operated by | Fails how | You observe it via |
|---|---|---|---|
| Anycast routing to nearest location | Platform | Reroutes to the next location | — |
| TLS termination | Platform | Certificate issues surface at the domain | sf list_custom_domains |
| DDoS mitigation, WAF | Platform | Fails closed (blocks) | Request logs |
| Your firewall rules, blocked paths, maintenance mode | You | Fails closed by design | sf firewall show, logs |
| Edge cache | Platform | Falls through to your Worker | x-sf-cache header |
| Worker isolate | Platform runs it, you wrote it | 5xx, exception, CPU limit | Logs, x-sf-version |
| Bindings (D1, KV, DO, R2) | Platform | Excluded from the Workers SLO | Logs, query metadata |
That last row is the one to carry forward into Failure domains.
The cache, as a state machine
The x-sf-cache header reports exactly one of these, and knowing which one you're in resolves most "why is this stale / why is this slow" questions:
Nothing is cached by default. A response is stored only when it carries Cache-Control: public, s-maxage=<n> and sets no cookies, and requests carrying a session cookie or Authorization header bypass the cache regardless. The mechanics, including which cookies are treated as inert, are in Routing and pages.
Every deploy invalidates the app's entire edge cache. That's the freshness guarantee: a release can never leave old HTML being served indefinitely. It also means the first requests after a deploy are all misses, which is visible as a brief traffic spike into your Worker.
Reads: which copy answered
For a database read, there's one more branch. D1 can serve reads from regional replicas, which multiplies read throughput but introduces the possibility of reading a slightly older copy.
The scaffold wires this up: query c.get('db') and each visitor carries a bookmark so they read their own writes. Where cross-user freshness is mandatory — checkout, inventory decrement, uniqueness enforcement — you read the primary explicitly. Full rules in Consistency and correctness.
Query result metadata reports where a read actually ran, which is how you verify replica behavior instead of assuming it.
Serial execution: the performance fact with availability consequences
A D1 database executes queries serially. One slow query does not slow one endpoint — it delays every request queued behind it.
The practical consequence is that an unindexed column on a hot query path is an availability risk, not a latency nuisance, and it degrades gradually as data grows: fine at a hundred rows, catastrophic at a hundred thousand. Two obligations follow — index every column used in a WHERE, JOIN, or correlated subquery, and verify with the query planner rather than intuition:
sf d1 select "EXPLAIN QUERY PLAN SELECT * FROM orders WHERE customer_id = 1"
A SCAN on a table beyond a few hundred rows means a missing index. Database covers the full performance discipline.
Observing a real request
# Which version answered, and was it cached?
curl -sSI https://your-app.example.com/ | grep -i 'x-sf-'
# What the Worker saw
sf logs --since 15m --errors
sf logs --since 15m --expand
# Request and response bodies (opt-in per app, 14-day retention)
sf firewall set --body-capture on
sf logs body <rayId>
Bodies are not captured by default — that's a deliberate privacy default, not an omission. When you do enable capture, it holds JSON and text only, with secrets redacted.
Next
- Compute and isolates — what actually executes your code, and its hard limits.
- Choosing a data store — where state belongs.
- Failure domains and resilience — what happens when a hop in this path degrades.
Frequently asked questions
How do we know whether a request ran our code?
The x-sf-cache response header. HIT means it was served from the edge cache without executing anything; MISS or BYPASS means your Worker ran. A cache hit also produces no request log, which is the usual explanation for 'missing' logs on a cached route.
Which location serves a given user?
The Cloudflare location nearest to them, chosen by anycast routing. You don't select it and it can change between requests. Your code is present in all of them after every deploy.
Can a request be rejected before it reaches our code?
Yes, in several places — DDoS mitigation, the WAF, your firewall rules, blocked paths, and maintenance mode all act before the isolate runs. That's a feature: filtered traffic costs you nothing.
How do we know which deployed version answered?
The x-sf-version response header names the deploy. Compare it with the id the deploy printed — this settles nearly every 'my change isn't live' question, especially inside the first minute after shipping.
Does a slow database query affect other requests?
It can. A D1 database executes queries serially, so one unindexed scan delays every request queued behind it. That makes an unindexed hot column an availability problem, not a slow-endpoint problem.