Skip to content

Configure the object cache

EmDash’s per-request cache already deduplicates identical reads during one page render. The optional object cache keeps selected query results across requests, allowing a later request to avoid the same database read. It is useful when public traffic creates more database reads than the selected backend should handle.

The database remains the source of truth. Cache reads fail open to the database, and writes invalidate the affected cache namespace. The object cache is disabled by default; enable it by adding an objectCache adapter to the emdash() integration.

Backend Best for Shared across isolates
KV Cloudflare Workers Yes
Memory Node.js, local development No (per process)

On Cloudflare, requests are served by many short-lived isolates across regions. KV is shared by all of them, so a value cached by one request is available to the next, anywhere. The memory backend caches within a single process, which suits a long-running Node.js server.

Configure the KV adapter and point it at a KV binding:

astro.config.mjs
import emdash from "emdash/astro";
import { d1, r2, kvCache } from "@emdash-cms/cloudflare";
export default defineConfig({
integrations: [
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
objectCache: kvCache({ binding: "CACHE" }),
}),
],
});

Create a KV namespace and add the binding to your Wrangler configuration.

Terminal window
npx wrangler kv namespace create CACHE

The command prints a namespace id. Add it under the binding name used in kvCache:

{
"kv_namespaces": [
{
"binding": "CACHE",
"id": "<namespace-id>"
}
]
}
Option Type Default Description
binding string — KV binding name from your Wrangler configuration. Required.
defaultTtl number 3600 Time-to-live for cached entries, in seconds. KV enforces a 60-second minimum.
revalidate number 1000 Isolate-local epoch-reuse window, in milliseconds. See Freshness.
timeout number 2000 Maximum time, in milliseconds, to wait for a KV read or epoch write. A timed-out read is treated as a cache miss or uses the last known epoch; a timed-out epoch write is logged without failing the mutation. Also bounds how long widget API mutations wait for epoch persistence. Set to 0 to disable.
keyPrefix string "em" Prefix for every cache key. Set a unique value when several sites share one namespace.

The memory adapter caches within the server process. It needs no external service:

astro.config.mjs
import emdash, { memoryCache } from "emdash/astro";
import { sqlite } from "emdash/db";
export default defineConfig({
integrations: [
emdash({
database: sqlite({ url: "file:./data.db" }),
objectCache: memoryCache(),
}),
],
});
Option Type Default Description
defaultTtl number 3600 Time-to-live for cached entries, in seconds.
revalidate number 1000 Isolate-local epoch-reuse window, in ms.
maxEntries number 1000 Maximum number of cached keys before older keys evict.
keyPrefix string "em" Prefix for every cache key.

The object cache covers the reads that run on a typical page render:

  • Content queries: getEmDashCollection, getEmDashEntry, and resolveEmDashPath.
  • Site settings, navigation menus, widget areas, and taxonomy terms.

Admin API requests, media files, and full HTML responses are not handled here. To cache rendered HTML, see Route Caching.

The object cache and an HTML edge cache solve different problems. A hit in the HTML layer does not run EmDash. A miss runs the Worker, and a response that may fill Astro’s route cache bypasses the object cache. Its content queries read from the database, which prevents a page purged after a content change from being rebuilt with an older KV snapshot.

Editing content through the admin panel or the REST API invalidates the affected cache entries automatically. Creating, updating, publishing, or deleting an entry clears the cached queries for its collection, including navigation menus that link to entries in it; changing a byline or taxonomy term clears the entries and menus that display it.

Widget changes made in the admin, by seeding, or by a site transfer clear the cached widget areas. Widget API mutations wait for the backend epoch write attempt before purging routes and returning a response. Code that writes the widget tables directly must call invalidateWidgetObjectCache() from emdash afterwards.

By default, invalidateObjectCache(namespace) and invalidateWidgetObjectCache() stamp the local epoch immediately and schedule the backend write without waiting for it. If your code also purges cached routes, await invalidateObjectCache(namespace, { waitForPersist: true }) or invalidateWidgetObjectCache({ waitForPersist: true }) before the purge. This option bypasses held writes inside coalesceObjectCacheWrites(). Epoch writes use the configured timeout (default two seconds); failures and timeouts are logged rather than thrown, so a successful mutation is not reported as failed. Waiting orders the epoch write attempt before the purge, but the shared change marker and its retries remain background work. Isolates that revalidate through the site-wide change marker can still reuse an old epoch until that background marker update becomes visible. Waiting for the epoch write does not cover those readers or remove KV propagation delays and isolate-local epoch reuse. Tests that read widgets immediately after an API mutation should poll for the updated data.

Inside transaction work that defers invalidations, waitForPersist only queues the invalidation; it does not bypass the transaction barrier. Call the invalidation helper with waitForPersist: true after the transaction has settled if you need to wait for persistence before purging routes.

For requests that are not route-cache fills, a change takes time to appear across all isolates as they pick up the bumped epoch. With the in-isolate memory backend this is immediate. With Workers KV it is bounded by KV’s edge-cache propagation (eventual consistency, up to ~60 seconds) plus the isolate-local revalidate window (default one second). Lower revalidate for faster local propagation at the cost of more reads against the cache; raise it to read the cache less often.

Every change other than to comments also rewrites a single site-wide change marker. When an isolate’s epochs are older than revalidate, it can check that marker instead: if nothing has changed, it keeps all of its epochs, so a render past the window costs one epoch read instead of one per collection and shared namespace. An isolate still re-reads its epochs for two minutes after each change, which keeps the KV propagation bound above, and at least every ten minutes. The comments epoch is always read directly. With revalidate: 0, every query reads its epochs directly.

Scheduled entries become visible when their publish time passes. A cached page reflects a newly-published scheduled entry on the next change to its collection, or when the cached entry’s defaultTtl lapses. If precise scheduled publishing matters for your site, set a lower defaultTtl.