Workers Cache: a cache in front of your Cloudflare Worker

By

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.

A quick way to confirm the cache is doing its job: run npx wrangler tail and hit the same URL twice. A cache hit never runs your Worker, so the second request produces no log line at all.

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.

Every Worker with caching enabled gets tiering for free, with no extra configuration.

Caching between Workers

The cache sits in front of every Worker entrypoint, not only the public URL. This includes calls between Workers over service bindings and 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 } }
  }
}

A typical setup uses an outer entrypoint that authenticates every request and is never cached. It calls a cached inner entrypoint that does the expensive work. This keeps authentication on every request while caching the slow part in the same 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.

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about cloudflare: