Skip to content
FLAVIO COPES
flaviocopes.com
2026

Workers Cache: a cache in front of your Cloudflare Worker

By Flavio Copes

Cloudflare launched Workers Cache, a tiered cache that sits in front of your Worker. How to enable it, set Cache-Control headers, and purge by tag.

~~~

Cloudflare just launched Workers Cache, and it fixes something that always bugged me about running apps on Workers.

Here’s the problem. When your Worker is your app, there’s nothing in front of it. Every request runs your code. Even when the response is identical to the one you returned a second ago, you still pay the render time and the CPU cost.

Workers Cache puts Cloudflare’s cache in front of the Worker. On a cache hit, your code doesn’t run at all. Cloudflare returns the cached response directly, and you pay zero CPU time for it.

Enabling it

One line in your wrangler.jsonc:

{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2026-05-01",
  "cache": { "enabled": true }
}

That’s it. No zone settings, no cache rules, no separate product to configure.

You control it with headers

Once the cache is enabled, you configure it the way HTTP always intended: with Cache-Control headers on your responses.

export default {
  async fetch(request) {
    const html = await renderPage(request)

    return new Response(html, {
      headers: {
        'Content-Type': 'text/html; charset=utf-8',
        'Cache-Control': 'public, max-age=300, stale-while-revalidate=3600',
      },
    })
  },
}

This says: treat the response as fresh for 5 minutes, and for up to an hour after that, keep serving the stale copy while refreshing it in the background.

The stale-while-revalidate part is what makes it feel instant. Without it, the first request after the cache expires waits for a full render. With it, that user gets the stale page immediately, and the Worker refreshes the cache in the background. Nobody waits.

I wrote more about how these headers work in HTTP caching.

Purging when content changes

You can tag responses with a Cache-Tag header:

return new Response(body, {
  headers: {
    'Cache-Control': 'public, max-age=300',
    'Cache-Tag': 'products,product:123',
  },
})

Then when the data changes, your Worker purges its own cache:

await ctx.cache.purge({ tags: ['product:123'] })

The purge is scoped to your Worker. No risk of accidentally wiping cached content for the rest of your zone.

It’s tiered by default

Workers Cache is a two-layer cache. There’s a lower tier in the data center closest to the user, and an upper tier that aggregates fills across the whole network.

The nice consequence: the first request from anywhere in the world populates the upper tier. After that, requests from any location can be served from cache, even if their local data center has never seen that URL before.

You don’t configure any of this. Every Worker with caching enabled gets tiering for free.

Caching between Workers

This is the part I find most interesting. The cache doesn’t just sit in front of the public URL. It sits in front of every Worker entrypoint, including calls between Workers over service bindings, and even calls between entrypoints in the same Worker.

You decide per entrypoint which ones cache:

{
  "cache": { "enabled": true },
  "exports": {
    "default": { "type": "worker", "cache": { "enabled": false } },
    "CachedBackend": { "type": "worker", "cache": { "enabled": true } }
  }
}

The typical pattern: an outer entrypoint that authenticates on every request (never cached), calling an inner entrypoint that does the expensive work (cached). You get auth on every request and a cache in front of the slow part, in one Worker.

For per-user data, the caller’s ctx.props becomes part of the cache key. Pass a userId in the props and each user gets their own cache entries. One user can never see another user’s cached response.

Billing

Cache hits don’t bill CPU time. They count as a normal request at the standard rate, but your Worker doesn’t run.

One thing to watch: with caching enabled, requests that used to be free, like static asset requests and worker-to-worker calls, are billed at the standard request rate, because each one now consults the cache.

Astro support is already there

If you deploy Astro on Cloudflare, the adapter wires this up for you:

// astro.config.mjs
import { defineConfig } from 'astro/config'
import cloudflare from '@astrojs/cloudflare'
import { cacheCloudflare } from '@astrojs/cloudflare/cache'

export default defineConfig({
  adapter: cloudflare(),
  output: 'server',
  experimental: {
    cache: { provider: cacheCloudflare() },
    routeRules: {
      '/products/*': { maxAge: 300, swr: 3600, tags: ['products'] },
    },
  },
})

Server-rendered pages get the render-once-then-cache flow automatically. Integrations for other frameworks are on the way.

Workers Cache is available today on every plan, including the free one. If you run anything server-rendered on Workers, add "cache": { "enabled": true } and set some headers. It’s the cheapest performance win you’ll get this year.

~~~

Related posts about cloudflare: