How it’s built

Why the menu is cached by version, not by time

The menu is read far more often than it changes. The bakery caches it under a key that names its version, so a new price is served from the first read after it is published and an old one is never served again, with no expiry to wait for and nothing to delete. This is how, what it leaves out, and what it costs.

A price, and when people see it

The seed’s menu has 24 items, and the country loaf costs $9.50 (crates/seed/src/menu.rs). A member of staff in the console sets it to $10.00. That is a draft: the Catalog object, one per sandbox, holds it as pending, and every reader still gets $9.50. Pressing Publish makes the drafts the menu, adds one to the menu’s version and records MenuPublished. From the next read on, everyone gets $10.00. The question for a cache is what happens between those two moments, and the bakery’s answer is that nothing does: there is no between.

What the Cache Lab shows, and what it cannot

The Cache Lab, as read on 2026-11-06, sends about 300 reads a second at a menu held in four cache nodes in front of a database with eight connections. Every entry has a time to live, and when the menu’s runs out the readers pile onto the database at once. Its switches are the usual answers: coalesce the misses, serve the stale copy while one fetch refreshes it, refresh early, jitter the expiry. Its writes either invalidate the entry or leave it, and it asks you to count the readers who get the old version until it expires.

The lab has no versioned keys and no ETag, and its cache is one place. Each of those is where the bakery differs, and the third is where the real platform differs from any simulation of one cache.

How a read works

GET /v1/sandboxes/…/menu (workers/api/src/http/menu.rs) first asks the Catalog for its version, a small read that the one object answering it makes strongly consistent: a publish that has committed is the version the next read sees. Then:

Nothing to invalidate

Invalidation is the work of finding every copy of something that changed and removing it in time. With the version in the key there is no such work. After the publish, every read asks for v2; the entry for v1 is still in the cache and is never asked for again. Correctness does not depend on anything being deleted, so nothing has to be deleted on time, and a delete that fails costs nothing but space.

The bakery does delete the old entry, as housekeeping: the consumer of MenuPublished removes the previous version’s key (workers/api/src/queue.rs). Cloudflare’s documentation, read on 2026-11-06, says a delete purges only the data center the Worker ran in, so this frees the space where the consumer happened to run and nowhere else. Elsewhere the old entry ages out on its own: the code stores every entry with max-age=86400, a day, because no sandbox outlives the next 10:00 UTC. That is the only time-based rule in the menu’s cache, and it decides when space is freed, never what a reader sees.

The browser’s cache has a rule of its own

The menu’s answer carries Cache-Control: private, no-cache, the one route allowed something other than no-store (workers/api/src/http/headers.rs). Under RFC 9111 that lets the browser keep its copy, and only its own, but never use it without asking first. The menu screen sends the ETag back each time, and an unchanged menu costs a 304 and no body. That is a second cache with a second key: the Cache API is keyed by the version on the server, the browser’s by the ETag it holds, and both are the same number. A 304 carries no Bakery-Cache header, because the Cache API was never asked.

One miss for every data center

Cloudflare’s pages, read on 2026-11-06, are plain that the Cache API is local to a data center: what one stores does not replicate outside it, and the Cache API does not use tiered caching. So v2 misses once in every data center that serves the sandbox, not once in the world. A visitor’s requests usually reach one data center, so in practice it is one miss a publish. Two reads that miss at the same moment in one data center both build the menu, because nothing coalesces them; at one visitor’s traffic that is rare and cheap.

What the bakery does not use

No Workers Cache on bakery-web, the Worker that serves the storefront’s pages. As Cloudflare’s documentation stated it on 2026-11-06, requests to static assets are free, and with Workers Cache on, every request is billed at the standard rate, static assets included. CI fails any wrangler.jsonc that names the key (.github/workflows/ci.yml). The public /menu/ page, before a sandbox exists, is the seed’s menu built into the HTML, so it costs nothing to serve and has nothing to cache. And no expiry on the menu besides the day-long max-age: a time to live is how a cache guesses when its copy has gone stale, and a key that names the version never has to guess.

What it costs

Try it

Open the menu with a sandbox: its “How this works” line names the version, the Bakery-Cache header and the ETag. Press Ask again and the answer is a 304. Sign in, change a price in the console, and ask again: still the old version, since a draft is not the menu. Publish it and ask once more: a new version, a new ETag and Bakery-Cache: miss, then a hit, or a 304, from then on.

Sources

Back to the Cache principle on How it’s built, to the decision that chose it, ADR 0003, or to the lab.