Cache

Avoid repeating expensive reads and computation with a small JSON cache on Cloudflare.

On this page

Available in Goribu 0.9.0. This guide describes request-scoped JSON caching through req.cache.

Read and cache

Read a value with req.cache, load it from its source on a miss, then cache the result. The D1 query below needs a configured database and an existing polls table; see D1 for setup.

src/routes/polls.ts
import type { Handler } from "goribu/server";

type Poll = { id: number; title: string };

export const GET: Handler = async (req, res) => {
  const key = "/polls/recent:v1";
  let polls = await req.cache.get<Poll[]>(key);
  if (polls === null) {
    polls = await req.d1<Poll>`SELECT id, title FROM polls ORDER BY id DESC LIMIT 20`.all();
    await req.cache.set(key, polls, { ttl: 300 });
  }
  return res.json({ polls });
};
src/routes/polls.js
export const GET = async (req, res) => {
  const key = "/polls/recent:v1";
  let polls = await req.cache.get(key);
  if (polls === null) {
    polls = await req.d1`SELECT id, title FROM polls ORDER BY id DESC LIMIT 20`.all();
    await req.cache.set(key, polls, { ttl: 300 });
  }
  return res.json({ polls });
};
  • Availability: req.cache is available in handlers and middleware without a binding or configuration.
  • Cache misses: Check misses with === null. Cached false, 0, and empty strings are valid results.
  • Absent results: Stored null cannot be distinguished from a miss. Use an object such as { found: false } to cache an absent result.

Operations

CallBehavior
get(key)Returns parsed JSON, or null on a miss or cache failure.
set(key, value, options?)Attempts to store JSON. The only option is ttl, in seconds, defaulting to 60.
delete(...keys)Attempts to remove one or more entries in the current data center.
  • Completion: Await each cache operation. set and delete resolve to undefined.
  • TTL validation: TTL must be a positive safe integer. Zero, fractions, negative values, and non-finite numbers are rejected; there is no permanent setting.
  • Retention: Entries can be evicted before TTL expiry. A resolved write does not guarantee Cloudflare retained it.
  • JSON conversion: Dates become strings, undefined object properties are omitted, and non-finite numbers become null.
  • Rejected values: Circular values, BigInts, and top-level values producing no JSON, such as undefined, functions, or symbols, are rejected.
Typed reads
type Poll = { id: number; title: string };

const polls = await req.cache.get<Poll[]>("/polls/recent:v1");
// Poll[] | null. The type argument does not validate stored data.
Typed reads
const polls = await req.cache.get("/polls/recent:v1");
// Poll[] | null. The type argument does not validate stored data.
  • Read type: Without a type argument, cache reads return unknown.
  • Public type: GoribuCache is available from goribu/server.

Keys and scope

  • Key format: Cache keys are valid Unicode strings starting with /, such as /polls/42. They need not identify a route.
  • Literal identity: /polls/../42, /42, /42#summary, and /42?full remain distinct keys.
  • Cache scope: Requests to the same origin share entries within a data center. Different schemes, hosts, or ports have separate entries.
  • HTTP independence: Goribu uses a named cache separate from the default HTTP cache. Storing a value does not publish it at that URL or change response cache headers.
  • Personalized entries: Users on the same origin share keys. Include the user or tenant identifier in personalized keys, and authorize the request before reading them.
  • Shape changes: Use versioned keys when changing the stored JSON shape across deployments.

Refresh after mutations

After a successful database mutation, delete the affected entries or replace a local entry with the updated value you already have:

Refresh affected entries
// After the database mutation succeeds:
await req.cache.delete("/polls/recent:v1", `/polls/${id}`);

// Or, when the mutation already returned the updated record:
await req.cache.set(`/polls/${poll.id}`, poll, { ttl: 300 });
Refresh affected entries
// After the database mutation succeeds:
await req.cache.delete("/polls/recent:v1", `/polls/${id}`);

// Or, when the mutation already returned the updated record:
await req.cache.set(`/polls/${poll.id}`, poll, { ttl: 300 });
  • Mutation confirmation: Keep the authoritative mutation result to confirm a successful write.
  • Concurrent fills: Cache fills can race with writes or deletions. The cache does not coordinate requests or provide transactions.
  • Local invalidation: A cache update affects only the data center executing the Worker; it does not invalidate another location's copy.
  • Freshness window: Choose a TTL matching the staleness your app can tolerate. Neither req.cache nor KV provides global strong consistency.

Smart Placement

  • Placement benefit: Smart Placement can improve cache reuse and refresh behavior for localized apps by concentrating more reads and mutations near the app's data.
  • Shared execution location: Requests reaching the same execution location share its local cache and updates.
  • Placement limits: Other locations depend on their own updates or TTL expiry. Placement does not guarantee every request reaches one location.
  • Placement configuration: Use Cloudflare's Worker placement settings.

Background writes and failures

To return a response without waiting for a cache fill, register the write explicitly with req.waitUntil:

Fill the cache in the background
req.waitUntil(
  req.cache.set("/polls/recent:v1", polls, { ttl: 300 }),
);
Fill the cache in the background
req.waitUntil(
  req.cache.set("/polls/recent:v1", polls, { ttl: 300 }),
);
  • Explicit background work: Ordinary cache calls never schedule background work automatically.
  • Deletion timing: Await a deletion when the handler should wait for the local attempt to complete.
  • Invalid arguments: Invalid keys, values, or options reject with an actionable [Goribu] error.
  • Batch validation: Every key in a deletion batch is validated before deletion starts.
  • Platform failures: Platform failures and unreadable cached JSON produce a warning and become a miss or skipped write/delete. The handler can continue.
  • Missing Cache API: Reads miss and writes/deletes do nothing in a runtime without the Cache API. Argument validation still runs.
  • Local storage tests: Development runs in workerd with a local Cache API. Use that runtime for tests needing actual storage.

Deployment support

DeploymentCache API behavior
Custom domain or routeCloudflare documents support; Goribu's custom-domain fixture passed live checks.
workers.devGoribu's named cache passed live checks on the tested deployment, despite the no-op restriction in Cloudflare's R2 guide.
Dashboard editor and Playground previewsCloudflare documents that cache operations have no impact.
Worker fronted by Cloudflare AccessCloudflare documents the Cache API as unavailable.
  • Observed deployments: Live checks on October 2, 2026 confirmed hits, literal keys, origin isolation, deletion, and TTL behavior on one custom domain and its workers.dev origin.
  • Evidence limits: Those checks do not establish support for every deployment, previews, or Access.
  • Documentation discrepancy: The discrepancy with Cloudflare's R2 guide has no established explanation.
  • Platform restrictions: The Cache API reference documents the preview and Access restrictions.
  • Hit verification: Verify actual hits where caching matters. Unsupported deployments can accept writes without retaining them and return misses without throwing.
  • Insufficient signals: A resolved set or an absence of warnings does not prove a hit.
  • Separate product: Cloudflare's Workers Cache product does not establish support for the Cache API used here.

Static pages

  • Build-time reads: Explicitly prerendered GET handlers bypass cache reads and writes so the build reads source data.
  • Fixed output: Generated HTML and navigation props keep the build-time snapshot. Cache expiry or deletion does not refresh them.
  • Static updates: Rebuild and deploy to update static output. See pre-rendered pages.