Product Launch · XiaoHu Explains

Cloudflare launches Workers Cache: caching moves in front of every Worker, hits don't cost CPU

One line of Wrangler config plus standard HTTP cache headers — and the cache can now sit between any two entrypoints inside a Worker.
Source · Cloudflare Blog Read · 7 min Cloudflare Workers · Edge caching · Serverless
30-second recap
  • Cloudflare launched Workers Cache today, putting a tiered regional cache 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 isn't billed for CPU time; on a miss, it runs once and backfills both cache layers.
  • The cache no longer lives on a zone (domain) — it travels with the Worker. workers.dev, preview environments, and Workers for Platforms all get it, and purging only affects your own entrypoint.
  • The biggest leap: the cache can now sit between any two entrypoints inside the same Worker, and with ctx.props it lets authenticated per-user APIs share a cache safely.
  • Available today for every Worker on every plan — no separate product, no extra line item.
Stance note: this article is adapted from Cloudflare's official launch blog. Claims like "hits carry zero CPU billing," "~50ms to reach roughly 95% of the world's internet population," and "we haven't seen another platform do this" are the vendor's own framing.
1 What it is

Now the Worker itself can have a cache

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

When the cache hits, your Worker doesn't run at all — this request isn't billed for CPU either. Only on a miss does the code run once, then it stashes the result in cache on the way out. The next request, from anywhere on Earth, can read straight from cache.

Why it matters: per Cloudflare's own account, they haven't seen another platform embed a cache inside a single deployment unit, switchable per entrypoint — nor another CDN turn "safe shared caching for logged-in-user APIs" into a built-in capability.

Turn 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 goes back to the way HTTP has always worked: set headers on the response.

Standard headers control caching behavior and tags
return new Response(body, {
  headers: {
    "Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
    "Cache-Tag": "products,product:123",
  },
});
Content changed — the Worker purges its own cache by tag
await ctx.cache.purge({ tags: ["product:123"] });

That's the whole API. No zone to configure, no rules engine to build, no separate cache product to provision, no second product to log into. The Worker's code is the config surface, and 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 all 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 roughly 95% of the world's internet population
512MB
Cacheable response size cap currently shared across all plans (to be split by plan later)
2 Background

The Worker used to stand in front of the cache — now it is the origin

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 sat in front of both the cache and the origin.

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

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

2017 · Old architecture
Request Worker Cache Origin
The Worker intercepts traffic first; the cache sits behind it, in front of the origin.
Now · Workers Cache
Request Cache Worker (is the origin)
The cache sits in front; on a hit it returns directly and the Worker never runs.

When the Worker is the origin, the old architecture with cache behind it has nothing left to cache. Every request re-runs the code, even if the response is byte-for-byte identical to the one from a second ago. The Workers runtime is fast enough to render fresh on every request, handling tens of millions of requests a second without breaking a sweat — but "fast enough to render every time" still comes with two bills: 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 only had two uncomfortable options to pick from:

Fully pre-render at build time
Pages load fast, but every edit means rebuilding and redeploying the entire site. A docs site with a few thousand pages can take 5–10 minutes to build, and large e-commerce sites are worse — touch anything and the whole thing reruns.
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 server-side on demand, cache the result, refresh it on whatever TTL you choose. The first request for a new page still renders; after that, until the cache expires, every request gets served as if it were a static page. You get the speed of a static site without the build step, and the freshness of server-side rendering without the ongoing cost.

3 New architecture

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

Workers Cache is, by default, a two-tier regional cache — and this topology kicks in automatically, no configuration needed. Here's how a request decides which layer to check, and when your code finally gets bothered:

Request comes in Lower-tier cache The datacenter closest to the user Hit Return directly Miss Upper-tier cache Network-wide, fewer nodes Hit Return andbackfill lower tier Neither tier has it Run the Worker once Only this one request actually renders fresh Write the result back to both upper and lower tiers The very first request worldwide fills the upper-tier cache After that, requests from any datacenter can hit it directly, even one that's never seen it before
Two-tier regional cache: lower tier is local, upper tier aggregates network-wide. Once the first request fills the upper tier, the whole network reuses it.
Think of it like

A convenience store chain restocking: check your own store's shelves first (lower-tier cache); if it's not there, call the regional warehouse (upper-tier cache); if the warehouse doesn't have it either, order fresh from the factory (run the Worker). Once the factory makes a batch, both the shelf and the warehouse get restocked — nobody else has to bother the factory again.

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

4 Key mechanism

Nobody waits around at the moment the cache expires

stale-while-revalidate tells Cloudflare: once the cache expires, it's fine to hand the user the old copy immediately while quietly re-running the Worker in the background to get a fresh one. This one directive is what turns "we'll cache your Worker for you" 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 actually feel. With it, the first request after expiry gets the old page instantly (with a Cf-Cache-Status: UPDATING header), while the Worker runs in the background to refill the cache. Every user, including the one who triggered the refresh, gets a cache-speed response.

Cache just generatedTime →Getting stale
Fresh window
Within max-age
Cloudflare returns the cached copy directly.
Worker doesn't run
Stale window
Within stale-while-revalidate
The old content is served instantly while the Worker refreshes in the background. Nobody waits.
Worker runs in background
Double expired
Both windows have passed
Cloudflare runs the Worker on the spot to generate a fresh response — only this one request waits for the render.
Worker runs on the spot

You set the windows yourself, matched to how often your content actually changes:

A product catalog that changes every few minutes
max-age=300
stale-while-revalidate=3600
Visitors basically never wait, and the Worker refreshes often enough to keep things current.
A blog archive that almost never changes
max-age=86400
stale-while-revalidate=2592000
The Worker runs at most once a day per page.

Only the very first request for a brand-new page pays the full render cost. After that, from a visitor's point of view, the page behaves exactly like static output — while your Worker still decides how the page gets generated.

5 Conceptual shift

This cache follows the Worker, not the domain

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

Workers Cache changes the ownership: 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, file-extension lists, Page Rules — none of them apply to Workers Cache. The Worker's Cache-Control headers are the entire configuration.
  • The cache follows the Worker, not the hostname. A Worker bound to api.example.com, api.example.net, and also invoked via a service binding shares one cache across all three paths. A request for /users/42, no matter which path it arrives on, hits the same cache entry.
  • Works on workers.dev 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, with no cross-tenant interference. These used to be second-class citizens for caching — not anymore.
  • Purging only affects your own entrypoint. Calling purge only clears your Worker's own entrypoint cache — it won't accidentally wipe out other content on the zone, and one Worker's deploy won't clear another's data.

Whatever caching behavior you want, it's written in code: give some paths a longer TTL by branching max-age per path; bypass caching for certain requests by returning Cache-Control: private; control how the cache key is computed by controlling what goes into ctx.props, or by normalizing the URL in a gateway Worker. The Worker you've already written is the config surface.

6 The biggest leap

The cache can slot into any seam inside a Worker

What Cloudflare calls its biggest leap

Workers Cache sits in front of every Worker entrypoint: the default export, every named entrypoint, and even the call where one entrypoint inside the same Worker invokes another via ctx.exports. That last one 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 around. When one entrypoint calls another via ctx.exports, the cache evaluates that call just like it would a browser request.

Think of it like

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

A hit returns the cached response — the called entrypoint doesn't run at all; a miss runs it once and stores a cache entry keyed by its own entrypoint, path, query string, and ctx.props. The caller always runs, but the work it hands off gets cached independently. You decide entrypoint by entrypoint who gets cached: wrangler's exports section lets you toggle each one separately. Entrypoints doing authentication, normalization, and dispatch — like a gateway or router — should have caching turned off, so they always run and their own output is never served from cache.

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 bolt-on product
Signature diagram: the cache is a switch embedded mid-call-chain, 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 aren't easy to combine on other platforms: authenticate every request, cache an expensive backend behind a multi-tenant-safe cache key, and purge that cache when the underlying data changes. Caching is configured per entrypoint — the gateway has to run every time (for auth, and also because caching it would let requests skip that auth check), so the default entrypoint has caching off, and only the internal entrypoint 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 } }
  }
}
Expand the full code for this one Worker (gateway + cached backend + tag-based invalidation)
src/index.ts · two execution stages in one source file, with the cache sitting in between
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 a given user's cache. Purge scope is tied to the entrypoint that
  // calls it, so this must run inside CachedBackend, which 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
// never gets 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 auto-bypasses
    // the cache and nothing gets cached at all), 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>;

The whole thing is one Worker, one source file, one deployment. But it has two execution stages, and a small exports block turns caching off for the gateway and on for the backend, with the cache sitting between them — keyed per user, purged by tag on writes, serving stale content while it refreshes in the background. This caching stage isn't bolted on; it's a layer of the program, written in code.

The same shape applies elsewhere: wrap a Durable Object behind an entrypoint, skip it entirely on a read hit, go straight to the Durable Object on writes and purge the cache by tag; strip tracking parameters (?utm_source=…) in the gateway before forwarding, so the cache only ever sees a clean URL and every variant of a link collapses to the same cache entry. These entrypoints can be layered on top of each other, and the caching stage between any two of them isn't something you configure — it's just something you decide where to place.

7 Multi-tenant safety

Logged-in-user APIs can safely share a cache too

If you're caching an endpoint that returns different content per user — say, an API that returns each logged-in user's own data — you need to guarantee one user never sees another user's cached response. The old approach was "never cache authenticated requests," and Cloudflare bypasses caching by default for any request carrying an Authorization header. But "never cache anything" 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, gets stored as separate cache copies — user A's data is never returned as a cached response to user B.

User A → /me User B → /me Gateway auth Strip Authorization Put userId into ctx.props Cache entry · userId=A Cache entry · userId=B Same /me, stored separately — no cross-wiring
ctx.props writes "who is this" into the cache key — multi-tenant isolation is handled by the key itself.
The called backend: ctx.props.userId goes into the cache key, 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 separate, 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 cached backend Worker. The gateway runs on every request (it has to, to authenticate), but the expensive backend only runs when this user doesn't already have a cache entry. An API that used to be "uncacheable because it's authenticated" 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," and they haven't seen anyone else build this in as a native capability.

This also happens to produce a "close to the user + close to the data" two-tier architecture

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

User Worker A · close to the user Auth · routing · render shell CacheB's cache Worker B · close to the data DB query · render · heavy lifting Hit: user → A → hits B's cache → returns, never jumps to B
The cache as the seam: hot pages return straight from A, the data hop only costs anything on a miss.

You don't need to design a special architecture to get this — just split your app into two Workers, point one at the other with a service binding, and turn on caching in Worker B's wrangler config. That's it.

8 Content negotiation · monitoring · billing

How content negotiation, the observability dashboard, and billing work

Real applications rarely return the exact same bytes to every client. The same product page needs to return HTML to a browser and JSON to 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.

Multiple versions of one URL: the Vary header

This is easy without caching — the Worker just reads a request header and returns the right thing. It gets hard once caching enters the picture. Most caches give you two bad options: don't cache URLs that have multiple versions, or cache just 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. There's no allowlist for what you can put in Vary — whatever you list, Cloudflare stores variants keyed on that value as-is. One URL, two cached variants, the Worker writes each one once and then runs zero times for anyone after that.

Cache and Worker metrics live on the same dashboard

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

Billing: hits skip CPU, but two scenarios 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 rateNot billed
Cache MISS (Worker runs)Standard rateBilled
Cache BYPASS (Worker runs)Standard rateBilled
Static asset requestStandard rateNot billed
Worker-to-Worker callStandard rateBilled only if it runs

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

9 Getting started · roadmap

Available today, still being refined

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

Cloudflare has also listed what's still in progress:

  • Tighter coordination between Smart Placement and caching. Right now the upper-tier cache target and the Smart Placement target are chosen independently, so a full miss can bounce a request across two Cloudflare datacenters: one to check the upper-tier cache, another to run the Worker near the data. They're working on coordinating the two so a miss only makes one long-haul trip.
  • Raising the size cap. At launch, every response shares the free plan's 512MB cacheable size cap, regardless of account. This is temporary and will be split by plan after a few rollout steps.
  • More framework adapters. Astro already has built-in Workers Cache support, so server-rendered pages automatically flow through "render once, cache, refresh in the background." TanStack Start and Next.js (via Vinext) are also catching up.
  • A new ctx.cache.invalidate(). purge deletes matching responses from the cache outright; invalidate will mark them "stale" instead, so the next request can still get a fast, stale response via stale-while-revalidate while the Worker refreshes in the background.
A Worker used to run in front of the cache. Now it can run behind it too. Use whichever side you need — or, with 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 title "Your Worker can now have its own cache in front of it" (blog.cloudflare.com/workers-cache/). This piece is a Chinese-to-English explainer adapted from vendor launch content; code samples and figures are drawn from the original post, and the architecture diagrams, flowcharts, and timelines were drawn by the explainer site based on the original's descriptions. Claims such as zero CPU billing on hits, ~50ms to reach 95% of the world's internet population, and "we haven't seen another platform/CDN do this" are Cloudflare's own stated framing.