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.
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 });
};
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.cacheis available in handlers and middleware without a binding or configuration. - Cache misses: Check misses with
=== null. Cachedfalse,0, and empty strings are valid results. - Absent results: Stored
nullcannot be distinguished from a miss. Use an object such as{ found: false }to cache an absent result.
Operations
| Call | Behavior |
|---|---|
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.
setanddeleteresolve toundefined. - 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.
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.
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:
GoribuCacheis available fromgoribu/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?fullremain 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:
// 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 });
// 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.cachenor 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:
req.waitUntil(
req.cache.set("/polls/recent:v1", polls, { ttl: 300 }),
);
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
| Deployment | Cache API behavior |
|---|---|
| Custom domain or route | Cloudflare documents support; Goribu's custom-domain fixture passed live checks. |
workers.dev | Goribu's named cache passed live checks on the tested deployment, despite the no-op restriction in Cloudflare's R2 guide. |
| Dashboard editor and Playground previews | Cloudflare documents that cache operations have no impact. |
| Worker fronted by Cloudflare Access | Cloudflare 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.devorigin. - 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
setor 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.