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.

The Spikefrost Team30 Jul 20264 min read

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

One request, end to end. A cache hit returns without executing any application code; a miss runs the isolate, which reaches state through bindings.D1 / KV / DO / R2Worker isolateEdge cacheDDoS · WAF ·firewallNearest CF locationClientNo isolate. No queries. No logs.stored only if public+ s-maxage and nocookiesalt[cache HIT][MISS or not cacheable]alt[blocked][allowed]TLS handshake + HTTP request1inspect2403 / challenge — no code runs, no cost3cache lookup4cached response (x-sf-cache: HIT)5invoke6read / write via binding7result8response (+ Cache-Control?)9response (x-sf-cache: MISS)10
One request, end to end. A cache hit returns without executing any application code; a miss runs the isolate, which reaches state through bindings.

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:

Edge cache states. Only responses that opt in with a public s-maxage directive and set no cookies are ever stored; every deploy invalidates the whole app's cache.

response did not opt in
(or request had a session cookie / auth header)

cacheable, nothing stored yet

stored, within s-maxage

served at the edge, no code runs

past s-maxage,
within stale-while-revalidate

background revalidation completed

deploy invalidated the cache

deploy invalidated the cache

every request runs the Worker

BYPASS

MISS

HIT

STALE

Edge cache states. Only responses that opt in with a public s-maxage directive and set no cookies are ever stored; every deploy invalidates the whole app's cache.

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.

With a request session, a visitor always reads their own writes even when a replica serves the read. Without one, a replica may answer from a slightly older state.D1 replicaD1 primaryWorkerVisitorreplica must be at leastas fresh as the bookmarkPOST /order (write)1INSERT2ok + bookmark3200 (bookmark in session cookie)4GET /orders (read)5SELECT with bookmark6rows including the new order7correct, current view8
With a request session, a visitor always reads their own writes even when a replica serves the read. Without one, a replica may answer from a slightly older state.

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

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.