Reference

Cache

req.cache is the Cloudflare Cache API behind four methods: get, set, delete and raw keyed by URL paths. It is built for data that is expensive to produce but changes rarely: leaderboards, feeds, aggregate counts, rendered markdown, processed images, LLM output. Reach for it when a read costs real time and slight staleness is acceptable.

export async function GET(req, res) {
  let polls = await req.cache.get("/polls");
  if (!polls) {
    polls = await getAllPolls(req.d1);
    await req.cache.set("/polls", polls, { ttl: 300 });
  }
  return res.render(PollsList, { polls });
}

get returns the stored value or null on a miss. Keys are URL paths starting with / as that is how the underlying Cache API addresses entries and it keeps one mental model: you are caching "what lives at this URL." Values are JSON-serialized, so anything JSON.stringify accepts works.

Cache an expensive read

The flow above is the whole pattern: ask the cache and on a miss compute the value and write it. The first request pays the cost and warms the entry, while every request after serves from cache until the TTL lapses or you invalidate. Use it only where the read is genuinely expensive. A SELECT … WHERE id = ? already returns in a couple of milliseconds, so caching it just buys you staleness for nothing.

Keep it fresh after a write

The handler that mutates a resource knows exactly what went stale, so it is the place to invalidate. delete drops an entry, while set can warm the next read directly:

export async function POST(req, res) {
  const poll = await createPoll(req.d1, { title, options });
  await req.cache.delete("/polls");                       // the list is now stale
  await req.cache.set(`/polls/${poll.id}`, poll, { ttl: 3600 }); // warm the detail page
  return res.redirect(`/polls/${poll.id}`);
}

delete takes several keys at once when one mutation touches several URLs:

await req.cache.delete("/polls", "/polls/featured", "/admin/polls");

Explicit set/delete in your handlers is the real freshness mechanism — the TTL is only a backstop.

Cache per user, not globally

A key without the user in it leaks data across accounts. Always scope per-user entries:

// Wrong — every user reads the same dashboard.
const dashboard = await req.cache.get("/dashboard");
// Right — one entry per user.
const dashboard = await req.cache.get(`/dashboard/${userId}`);

TTL is a safety net

ttl is in seconds and defaults to 60. Treat it as a ceiling on staleness, not the primary mechanism. It exists to cover the delete you forgot, mutations from outside your app (a cron job, a manual SQL edit), and errors that land between the database write and the cache write. The short default heals forgotten invalidations quickly. Reach for longer TTLs when data genuinely changes rarely, and ttl: 0 for effectively-permanent content you will set/delete explicitly.

await req.cache.set("/leaderboard", board, { ttl: 60 });   // default
await req.cache.set(`/polls/${id}`, poll, { ttl: 3600 });  // an hour
await req.cache.set("/site/about", about, { ttl: 0 });     // until you change it

Going deeper

Per-POP storage

The Cache API stores entries in the data center that wrote them. A set in Frankfurt warms Frankfurt. Tokyo serves its own copy until its TTL lapses or a Tokyo write replaces it. For most apps this is the right trade: each POP warms independently and reads stay local and free. It also means invalidation is local: a delete from a European request won't clear the entry a request in Tokyo will hit.

If your users are worldwide but your database is regional, pair the cache with Cloudflare Placement Hints to pin the Worker near the database, so invalidations route through the same hot caches:

// wrangler.jsonc
{ "placement": { "region": "aws:us-east-1" } }

When you need globally-consistent, immediately-visible invalidation instead, use KV via req.env.MY_KV — globally replicated, with roughly 60-second write propagation.

The raw escape hatch

req.cache.raw is direct access to caches.default. Use it when you are outside the helper's surface. Custom Vary headers, specific Cache-Control directives, hand-built Response objects:

const match = await req.cache.raw.match(new Request(someUrl, { headers }));

When req.cache is the wrong tool

Skip it for cheap reads (already fast from the database), for time-sensitive data where any stale window is a bug (stock prices, auth tokens, real-time feeds), and for anything needing strong consistency. That is outside the Cache API's scope.

In tests

In environments without the Cloudflare Cache API (Vitest, Node, non-Worker runtimes) req.cache is a no-op stub: get returns null, set and delete resolve silently and raw is null. Handlers keep working, just uncached. A silent cache miss is harmless, so this fails quietly rather than throwing. Invalid keys (anything not starting with /) are skipped with a console warning in wrangler dev, so typos surface during development.