Product Launch · XiaoHu Explains

Cloudflare Ships Workers Cache: Caching Moves Right In Front of Every Worker, Hits Cost Zero CPU

One line of Wrangler config plus standard HTTP cache headers — and caching can now sit between any two entrypoints inside a single Worker.
Source · Cloudflare Blog Read · 7 min Cloudflare Workers · Edge Caching · Serverless
30-Second Recap
  • Cloudflare today shipped Workers Cache, wiring tiered regional caching directly in front of every Worker — one line of Wrangler config turns it on.
  • On a cache hit, the Worker doesn't run at all and incurs no CPU billing; only a miss executes the code, then backfills both cache tiers.
  • The cache is no longer tied to a zone (domain) — it follows the Worker. workers.dev, preview environments, and Workers for Platforms all get it, and purges only touch your own cache.
  • The biggest breakthrough: caching can now sit between any two entrypoints inside the same Worker, and combined with ctx.props it lets logged-in-user APIs safely share a cache.
  • Live today for Workers on every plan — no separate product, no extra line item.
Stance note: this piece is compiled from Cloudflare's official launch blog. Claims like "zero CPU billing on hits," "~50ms to reach ~95% of the world's internet population," and "we haven't seen another platform do this" are all the vendor's own framing.
1 What It Is

Now your Worker gets its own cache layer, too

Cloudflare today launched Workers Cache, a cache layer that sits directly in front of your Worker. Turning it on takes one line of config; controlling it uses the standard HTTP cache headers you already know.

On a cache hit, your Worker doesn't run at all, and this request incurs no CPU billing. Only on a miss does the code run once — and when it does, it saves the result into the cache on its way out. The next request, from anywhere on Earth, can read straight from the cache.

Why it matters: per Cloudflare's own framing, no other platform has embedded caching inside a single deployment unit with per-entrypoint switches, and no other CDN has built "logged-in-user APIs safely sharing a cache" in as a native capability.

Turning on caching — this is the entire config
{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2026-05-01",
  "cache": {
    "enabled": true
  }
}

Once it's on, how things get cached is handled the way HTTP has always done it: by setting headers on the response.

Standard headers control how it's cached and what tags it carries
return new Response(body, {
  headers: {
    "Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
    "Cache-Tag": "products,product:123",
  },
});
When content changes, the Worker purges its own cache by tag
await ctx.cache.purge({ tags: ["product:123"] });

That's the entire API. No zone to configure, no rules engine to build, no separate cache to provision, no second product to log into. Your Worker's code is the config surface — the cache follows the Worker wherever it runs: custom domains, workers.dev, behind a service binding, preview environments, inside a Workers for Platforms tenant. It's one Worker, one cache, configured once.

0
CPU time consumed and billed by the Worker on a cache hit
~50ms
Max latency radius for Cloudflare's network to reach ~95% of the world's internet population
512MB
Cacheable response size cap currently shared across all plans (to be tiered by plan later)
2 Background

Workers used to stand in front of the cache; now they're the origin themselves

When Cloudflare launched Workers back in 2017, the pitch was running your code at the network edge to modify a request on its way to the origin. Back then, the Worker stood in front of both the cache and the origin.

The origin is the server that actually holds your site's content and generates the page. With a Worker in front of it, you could add headers, rewrite URLs, run A/B splits, filter traffic before it ever reached the origin. For that era's use cases, that position made sense — you had full control over what should and shouldn't be cached.

Then things changed. Frameworks like Astro, TanStack Start, Next.js, Remix, and SvelteKit now ship Cloudflare adapters that package the entire application into a single Worker. There's no origin behind them — the Worker itself is that server.

2017 · Old Architecture
Request Worker Cache Origin
The Worker intercepts traffic up front; the cache sits behind it, guarding the origin.
Now · Workers Cache
Request Cache Worker (the origin)
The cache sits up front; a hit returns immediately and the Worker never runs.

When the Worker is the origin, the old architecture's cache — sitting behind it — had nothing left to cache. Every request had to re-run the code, even when the output was byte-for-byte identical to a second ago. The Workers runtime is fast enough to render fresh on every single request, tens of millions of requests a second without breaking a sweat — but "fast enough to render every time" still costs two things: latency on every page load, and CPU time on every execution. And for a server-rendered app, every page load is by definition a render.

Until now you had to pick between two options, neither of which felt great:

Fully pre-render at build time
Pages load fast, but every change means rebuilding and redeploying the whole site. A docs site with a few thousand pages can take 5 to 10 minutes; a large e-commerce site is worse — and touching anything means running it all again.
Render fresh on every request
Content is always current, but every page load pays the render cost, and every visitor pays the latency.

Workers Cache offers a third path: render on demand on the server, cache the result, refresh it on whatever lifetime you choose. A brand-new page's first request still renders; after that, until the cache expires, every request gets served like a static page. You get static-site speed without the build time, and server-render freshness without the ongoing cost.

3 New Architecture

The cache flips around — now it sits in front of the Worker

Workers Cache is, by default, a two-tier regional cache, and this topology works automatically — you don't configure any of it. Here's how a request decides which tier to check, and when it finally bothers your code:

Request arrives Lower-tier cache The data center closest to the user Hit Return directly Miss Upper-tier cache Network-wide aggregate, fewer nodes Hit Return andbackfill lower tier Neither tier has it Run the Worker once Only this request actually renders fresh Write the result back into both tiers The first request anywhere fills the upper tier After that, any data center can hit it directly, even one that's never seen it
Two-tier regional caching: the lower tier is local, the upper tier is aggregated. Once the first request fills the upper tier, the whole network reuses it.
An analogy

Think of a convenience store chain restocking: first check your own store's shelf (lower-tier cache); if it's not there, ask the regional warehouse (upper-tier cache); if the warehouse doesn't have it either, only then call the manufacturer to make it fresh (run the Worker). Once the manufacturer makes a batch, both the shelf and the warehouse get restocked — nobody has to bother the manufacturer again next time.

This topology is the same one zones use today for Tiered Cache — the difference is you don't configure it. There's no "turn on tiered caching for my Worker" switch. If the Worker has caching enabled, tiering comes free. And if your Worker uses Smart Placement, it stacks cleanly on top: both cache tiers are checked first, and only when neither hits does Smart Placement route execution to wherever the data lives.

4 Key Mechanism

Nobody waits at the moment a cache expires

stale-while-revalidate literally means "revalidate while stale." It tells Cloudflare: once the cache expires, it's fine to immediately serve the old copy to the user while quietly re-running the Worker in the background to get a fresh one. This single directive is what turns "we cache your Worker" into "your Worker-backed site feels like a static site."

Without it, the first request after expiry has to wait for the Worker to render from scratch — a delay users can feel. With it, the first request after expiry gets the old page instantly (tagged with a Cf-Cache-Status: UPDATING header) while the Worker refreshes the cache in the background. Every user — including the one who triggered the refresh — gets a cache-speed response.

Cache just generatedTime moves right →Getting staler
Fresh window
Within max-age
Cloudflare returns the cache directly.
Worker doesn't run
Stale window
Within stale-while-revalidate
The old content is served to the user immediately while the Worker refreshes it in the background. Nobody waits.
Worker runs in background
Doubly expired
Both windows have passed
Cloudflare runs the Worker on the spot to generate a fresh response; only this one request waits on the render.
Worker runs on the spot

You set the windows. Match them to how often your content actually changes:

A product catalog that changes every few minutes
max-age=300
stale-while-revalidate=3600
Visitors barely wait, and the Worker runs often enough to stay fresh.
A blog archive that almost never changes
max-age=86400
stale-while-revalidate=2592000
Each page's Worker runs at most once a day.

Only the first request to a brand-new page pays the full render cost. After that, from a visitor's perspective the page performs exactly like static output — while your Worker still decides how it's generated.

5 Conceptual Shift

This cache follows the Worker, not the domain

Cloudflare has always had caching, but it was configured at the zone level (a domain's configuration unit): Cache Rules, Page Rules, the cacheable-file-extension list, Cache Reserve, tiered-cache topology, custom cache keys — all set on the zone. In the past, a Worker either had to accommodate whatever the zone's config said, or work around it.

Workers Cache changes who owns it: it's your Worker's cache, owned by the Worker, not by some zone. That brings a few concrete benefits:

  • No zone config to manage. Cache Rules, cache-level settings, the file-extension list, Page Rules — none of it applies to Workers Cache. The Worker's Cache-Control header is the entire configuration.
  • The cache follows the Worker, not the hostname. One Worker bound to api.example.com, api.example.net, and also invoked via a service binding — all three paths share the same cache. Request /users/42 from any of them, and it hits the same cache entry.
  • workers.dev gets it too. Preview environments each get their own independent cache, so testing a change doesn't pollute production; each tenant Worker in Workers for Platforms caches independently without interfering with others. These spots used to be second-class citizens for caching — not anymore.
  • Purging only touches your own entrypoint. When you call purge, it clears only your Worker's entrypoint cache — it won't accidentally hit other content in the zone, and one Worker's deploy won't wipe another's data.

Whatever caching behavior you want is written directly into your code: give certain paths a longer lifetime by branching on the path to set different max-age values; return Cache-Control: private for requests that should bypass the cache entirely; control how the cache key is computed by deciding what you put into ctx.props, or normalize the URL up front in a gateway Worker. The Worker you've already written is the config surface.

6 The Biggest Breakthrough

Caching can slot into any seam inside a single Worker

What Cloudflare calls the biggest breakthrough

Workers Cache sits in front of every entrypoint on a Worker: the default export, every named entrypoint, and also the call when one entrypoint invokes another entrypoint inside the same Worker via ctx.exports. That last one is what changes what you can build.

A single Worker program can be split into several independent "entrypoints" that call each other directly in code, without actually firing off a network request and looping back. When one entrypoint calls another via ctx.exports, the cache evaluates that call exactly as it would a browser request.

An analogy

Like a company where the front desk (the gateway entrypoint) takes a visitor's request and passes it straight to the relevant internal department (another entrypoint) — the visitor doesn't have to walk across the street to another building. The cache sits right between the front desk and the department: if the department's last answer is still on file, the front desk hands it to the visitor directly, and the department doesn't even need to open its door this time.

A hit returns the cached response and the called entrypoint never runs; a miss runs once and stores a cache entry keyed by its own entrypoint, path, query string, and ctx.props. The caller still runs every time, but the work it hands off gets cached independently. You decide caching per entrypoint — wrangler's exports section lets you flip it on or off for each one. Entrypoints that do auth, normalization, or dispatch — like a gateway or router — should have caching disabled so they run every time, and their own output never gets served back as a cached response.

One Worker · One source file · One deployment Gateway entrypoint default · auth · routing Cache: off Cache switch Hit returns directly Internal compute entrypoint Heavy lifting · skipped on hit Cache: on Caching is a layer written into the code, not a bolted-on product
Signature diagram: caching is a switch embedded mid-chain in the call flow, toggled on or off per entrypoint.

How caching is configured is entirely expressed as ordinary Worker code: which entrypoint you call, what request you forward, what ctx.props you pass, what Cache-Control you set. The Worker below does three things at once that are hard to pull off together on other platforms: authenticate every request, cache an expensive backend behind a multi-tenant-safe cache key, and purge that cache when data changes. Caching is configured per entrypoint — the gateway must run every time (both for auth, and because caching it would let the cache skip the auth check), so the default entrypoint has caching off and only the internal one has it on:

Per-entrypoint toggles in wrangler: gateway off, internal compute entrypoint on
{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2026-05-01",
  "cache": { "enabled": true },
  "exports": {
    // The gateway must run on every request — don't cache it
    "default": { "type": "worker", "cache": { "enabled": false } },
    // Cache the expensive internal entrypoint
    "CachedBackend": { "type": "worker", "cache": { "enabled": true } }
  }
}
Click to see the full code for this Worker (gateway + cached backend + tag-based invalidation)
src/index.ts · Two execution stages in one source file, with caching sandwiched between them
import { WorkerEntrypoint } from "cloudflare:workers";

interface Env { API_TOKEN: string; }
interface Props { userId: string; }

// Internal entrypoint: the expensive work. The cache sits in front of it,
// so this code doesn't run at all on a hit.
export class CachedBackend extends WorkerEntrypoint<Env, Props> {
  async fetch(request: Request): Promise<Response> {
    // ctx.props.userId is part of the cache key, so each user gets
    // a separate cache entry.
    const { userId } = this.ctx.props;
    const data = await loadExpensiveData(userId);

    return new Response(JSON.stringify(data), {
      headers: {
        "Content-Type": "application/json",
        "Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
        "Cache-Tag": `user:${userId}`,
      },
    });
  }

  // Purge the cache for a given user. Purge scope is limited to the
  // entrypoint calling it, so this must run inside the CachedBackend
  // that owns the cache.
  async invalidate(userId: string): Promise<void> {
    await this.ctx.cache.purge({ tags: [`user:${userId}`] });
  }
}

// Outer entrypoint: runs on every request, handles auth and routing.
// Caching is disabled for it in wrangler, so it always runs and auth
// is never skipped by a cache hit.
export default {
  async fetch(request, env, ctx): Promise<Response> {
    const userId = await authenticate(request, env);
    if (!userId) return new Response("Unauthorized", { status: 401 });

    // On writes, purge this user's cache from the entrypoint that owns it.
    if (request.method === "POST") {
      await handleWrite(request, userId);
      await ctx.exports.CachedBackend.invalidate(userId);
      return new Response("OK");
    }

    // Reads: strip Authorization first (otherwise Cloudflare will
    // automatically bypass the cache and nothing gets cached), then
    // forward the authenticated user identity via ctx.props to the
    // cached backend.
    const forwarded = new Request(request);
    forwarded.headers.delete("Authorization");

    return ctx.exports.CachedBackend.fetch(forwarded, {
      props: { userId },
    });
  },
} satisfies ExportedHandler<Env>;

It's all one Worker, one source file, one deployment. But inside it there are two execution stages, and a small exports block turns caching off for the gateway and on for the backend, with the cache sandwiched between them — keyed per user, purged by tag on write paths, serving stale content while refreshing in the background. This caching stage isn't bolted on from outside; it's a layer of the program, written in code.

The same shape applies elsewhere: wrap a Durable Object behind an entrypoint, leave it untouched on read hits, write straight to the Durable Object and then purge the cache by tag; strip tracking params (?utm_source=…) at the gateway before forwarding, so the cache only ever sees a clean URL and every variant of a link collapses onto the same cache entry. These entrypoints can be stacked layer on layer, and the caching stage between any two of them isn't something you configure — it's just where you decide to put it.

7 Multi-Tenant Safety

Even logged-in-user APIs can safely share a cache

If you're caching an API that returns different content per user — say, an endpoint that returns each logged-in user's own data — you need a guarantee that one user can never see another user's cached response. The old fix was "never cache authenticated requests," and Cloudflare's default is exactly that: bypass the cache on any request with an Authorization header. But "cache nothing" throws away the entire performance win.

Workers Cache solves this by folding the caller's ctx.props into the cache key. Every cached request gets an invisible "who is this" tag; the same URL, requested by different users, each gets its own stored copy, and user A's data is never returned to user B as a cached response.

User A → /me User B → /me Gateway auth Strips Authorization Puts userId into ctx.props Cache entry · userId=A Cache entry · userId=B Same /me, stored separately, never crossed
ctx.props writes "who this is" into the cache key — multi-tenant isolation is handled by the cache key itself.
Called backend: ctx.props.userId goes into the cache key, so each user gets their own entry
export default class Backend extends WorkerEntrypoint<Env, Props> {
  async fetch(request: Request): Promise<Response> {
    // ctx.props.userId is part of the cache key. Users A and B
    // requesting the same URL get independent cache entries.
    const { userId } = this.ctx.props;
    const data = await loadUserData(userId);
    return new Response(JSON.stringify(data), {
      headers: {
        "Content-Type": "application/json",
        "Cache-Control": "public, max-age=300",
      },
    });
  }
}

The typical pattern: authenticate the request in a gateway Worker, strip the Authorization header, write the authenticated user ID into ctx.props, then call the cache-backed Worker. The gateway runs on every request (it has to, to do the auth), but the expensive backend only runs when this particular user doesn't yet have a cache entry. An API that used to be "uncacheable" because of auth becomes "cached per user, with full security isolation," and the isolation is handled for you by the cache key. Per Cloudflare, other CDNs force you to choose between "correct" and "high hit rate" — they say they haven't seen anyone else build this in natively.

A bonus: a two-tier architecture that's both "close to the user" and "close to the data"

There's a long-standing tension in web performance: you want your code running close to the user (the user-to-server round trip sits on the critical path), and you also want it running close to the data (every database query is another round trip). Pick one, and the other gets slower. The Cloudflare network already reaches roughly 95% of the world's internet population within about 50ms; paired with Smart Placement, code can also run right next to the data. The missing piece was exactly this caching layer.

User Worker A · close to the user Auth · routing · shell rendering CacheB's cache Worker B · close to the data DB queries · rendering · heavy lifting Hit: user → A → hits B's cache → returns, never even jumps to B
The cache as a seam: hot pages hit directly at A and return; the jump to the data only costs anything on a miss.

You don't need to design anything special to get this: write your app as two Workers, point one at the other with a service binding, turn on caching in Worker B's wrangler config, and that's it.

8 Content Negotiation · Monitoring · Billing

How content negotiation, the observability dashboard, and billing work

Real applications rarely return identical bytes to every client. The same product page needs HTML for a browser and JSON for an API client; the same image needs WebP for clients that support it and JPEG for those that don't; the same homepage might return Chinese, French, or Japanese depending on the user.

Storing multiple versions of one URL: the Vary header

Without caching this is easy — the Worker just reads a request header and returns the matching thing. It gets hard once caching is involved. Most caches only give you two bad options: don't cache a URL that has multiple versions at all, or cache one version and serve it to everyone. Workers Cache supports the standard Vary header: return Vary: Accept, and Cloudflare stores a separate variant per distinct Accept value, returning only the one that matches the incoming request. One URL, two cached variants — the Worker writes each one once, then runs zero times for anyone after that. There's no whitelist on which headers you can Vary by; whatever you list, Cloudflare keys the variant off that value as-is.

Caching and the Worker show up on the same dashboard

The Workers Observability dashboard now shows cache information per invocation. You can see hit-rate trends over time per Worker, along with a breakdown of HIT, MISS, UPDATING, and BYPASS. When hit rate is low, this is where you go to find out why: too many BYPASS results might mean something is setting a cookie; too many MISS results might mean your cache key is more fragmented than you thought; too many UPDATING results might mean max-age is shorter than your actual traffic interval. It all lives on the same dashboard as the Worker's logs, exceptions, CPU time, and request counts — no switching back and forth.

Billing: hits skip CPU charges, but two things now incur standard request fees

A cache hit doesn't run the Worker and isn't billed for CPU time, but it still counts as a request, billed at the standard Workers request rate, same as any other invocation. Misses and bypasses are billed normally: request fee plus CPU time.

ScenarioRequest feeCPU time fee
Cache HIT (Worker doesn't run)Standard rateNone
Cache MISS (Worker runs)Standard rateCharged
Cache BYPASS (Worker runs)Standard rateCharged
Static asset requestStandard rateNone
Worker-to-Worker callStandard rateCharged only if it runs

There's no separate Workers Cache line item and no per-GB cache storage fee. Tiered caching, purging, stale-while-revalidate, and the analytics above are all included. A request that would have run the Worker but gets served as a hit still costs you the standard request fee, but not the CPU time — so it's cheaper than rendering fresh in the Worker. One thing worth noting: once caching is on, static asset requests that used to be free, and Worker-to-Worker calls via a service binding or ctx.exports, are now billed at the standard request rate, because every one of them now has to check the cache in front of it first.

9 Getting Started · Roadmap

Available today, and Cloudflare is still refining it

Workers Cache is live today for every Worker on every plan, enabled through Wrangler. Getting started takes three steps: add "cache": { "enabled": true } to wrangler.jsonc, redeploy, then start setting Cache-Control headers on your responses.

Cloudflare also lists a few things that are still in progress:

  • Tighter coordination between Smart Placement and caching. Right now, the upper-tier cache location and the Smart Placement target are chosen independently, so on a total miss, a request might make two hops across Cloudflare data centers: one to check the upper-tier cache, one to run the Worker near the data. They're working on coordinating these two choices so a miss only makes one long hop.
  • Raising the size cap. At launch, every response shares the free plan's 512MB cacheable size cap, regardless of account tier. This is temporary and will be tiered by plan after a few rollout stages.
  • More framework support. Astro already has built-in Workers Cache support, so server-rendered pages automatically get the "render once, cache, refresh in the background" flow; TanStack Start and Next.js (via Vinext), among others, are catching up.
  • A new ctx.cache.invalidate(). purge deletes matching responses from the cache outright; invalidate marks them "stale" instead, so the next request can still get a fast, old response via stale-while-revalidate while the Worker refreshes it in the background.
Workers used to run in front of the cache. Now they can run behind it, too. Use whichever side you need — or, via a service binding, both at once. — Cloudflare Blog, "Your Worker can now have its own cache in front of it"
Source: The Cloudflare Blog, published July 6, 2026, original article "Your Worker can now have its own cache in front of it" (blog.cloudflare.com/workers-cache/). This piece is a Chinese-language explainer of vendor launch content; code samples and figures are drawn from the original. Architecture diagrams, flowcharts, and the timeline in this piece were drawn by the explainer site based on the original's descriptions. Claims such as zero CPU billing on hits, ~50ms reach to 95% of the world's internet population, and "we haven't seen another platform/CDN do this" reflect Cloudflare's own official framing.