---
title: "Configure caching"
description: "What the cache stores by default, when to change it, and how to control freshness, TTL, and the driver."
canonical_url: "https://content.comark.dev/advanced/caching"
---
# Configure caching

> What the cache stores by default, when to change it, and how to control freshness, TTL, and the driver.

The cache is on by default and needs no setup. It keeps parsed page bodies and the content index in memory, so a page is parsed once per process and later reads return the stored copy. This page is for when that default isn't what you need.

## When to configure it

Leave the default alone while you develop and for most single-server deployments. Reach for the `cache` option when:

- **Several processes should share parsed pages.** Serverless functions and multi-instance servers each start with an empty in-memory cache. Point `driver` at Redis, a platform runtime cache, or another shared store, and a cold instance restores the index and warm pages instead of rebuilding them.
- **Remote content should refresh on its own.** A [GitHub](https://content.comark.dev/sources/github) or [storage](https://content.comark.dev/sources/unstorage) source has no watcher. Set a `ttl` and stale pages are re-read in the background as they're requested.
- **You want every read to hit the source.** Pass `cache: false`. Useful for tests, and a fair choice when a [snapshot](https://content.comark.dev/sources/snapshot) already serves parsed content.

For local edits during development, you don't need any of this: the [watcher](https://content.comark.dev/guide/watch) updates cached pages as files change.

## Options

Pass a [`CacheOptions`](https://content.comark.dev/reference/types/content#cacheoptions) object as `cache` to [`comarkContent()`](https://content.comark.dev/reference/content/comark-content), or `false` to disable it:

```ts [content.ts]
const content = comarkContent({
  source: fs('./content'),
  cache: {
    ttl: 1000 * 60 * 60, // 1 hour
    swr: true, // default
  },
})
```

| Option                      | Type                                          | Default     | Description                                                  |
| --------------------------- | --------------------------------------------- | ----------- | ------------------------------------------------------------ |
| [`driver`](#options-driver) | [`Driver`](https://unstorage.unjs.io/drivers) | in-memory   | unstorage driver backing the cache.                          |
| [`ttl`](#options-ttl)       | `number`                                      | `undefined` | Milliseconds before an entry is stale. Omit to never expire. |
| [`swr`](#options-swr)       | `boolean`                                     | `true`      | Serve a stale entry while it revalidates in the background.  |

### `driver`

Any [unstorage](https://unstorage.unjs.io) driver. Defaults to an in-memory driver, which is per-instance. Point it at Redis, a Vercel Runtime Cache, or anything unstorage supports to share the cache across instances:

```ts [driver.ts]
import redisDriver from 'unstorage/drivers/redis'

comarkContent({
  source: fs('./content'),
  cache: { driver: redisDriver({ /* ... */ }) },
})
```

### `ttl`

Milliseconds before a cached entry is considered stale. Omit it (the default) and entries never expire on their own. What happens once an entry is stale is governed by [`swr`](#options-swr).

### `swr`

Stale-while-revalidate, on by default. A read of a stale entry returns the cached copy immediately and runs a [`refresh()`](https://content.comark.dev/reference/content/refresh) in the background, so the source is re-read, added and removed files are reconciled, and fresh content lands on the following read. It's lazy, not a timer: nothing refreshes until an entry is read again after `ttl`.

Set `swr: false` and a stale entry counts as a miss instead, so the read waits for the source.

With no `ttl`, entries never go stale and this option has nothing to act on.

::note
Starting an instance from stored data is a [source](https://content.comark.dev/sources/snapshot) concern rather than a cache option; see [Artifacts and hydration](https://content.comark.dev/advanced/artifacts-and-hydration).
::

## Refresh on demand

When content changes and no watcher is running (the usual case for remote sources), refresh it yourself from a webhook or admin action:

| Method                                                                                                 | Effect                                                                                             |
| ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| [`content.refresh()`](https://content.comark.dev/reference/content/refresh)                            | Re-read the source: re-parse every entry and reconcile the index, dropping files that disappeared. |
| [`content.get(path, { fresh: true })`](https://content.comark.dev/reference/content/get#options-fresh) | Re-read one page from the source now and store the new result.                                     |
| `content.cache.invalidate(key)`                                                                        | Remove one entry (`<name>:<path>`) so the next read re-parses it.                                  |
| `content.cache.expire(key)`                                                                            | Mark one entry stale without removing it, so `swr` refreshes it on the next read.                  |

```ts [refresh.ts]
await content.refresh() // re-read everything
await content.cache.invalidate('default:posts/hello.md') // drop a single entry
```

For a production pattern that replaces the instance per commit instead of refreshing it, see [Keep remote content up to date](https://content.comark.dev/deployment/remote-content).

## Disable

Pass `cache: false` and every `content.get()` re-parses from the source:

```ts [off.ts]
comarkContent({ source: fs('./content'), cache: false })
```

With the cache off, the index is rebuilt in memory on each instance and never persisted, so [`list()`](https://content.comark.dev/reference/content/list) and [`navigation()`](https://content.comark.dev/reference/content/navigation) still work, though without the cold-start shortcut.

A [parsed source](https://content.comark.dev/sources/snapshot) behaves consistently: with nowhere to store hydrated bodies, each read re-reads the stored data, exactly as a raw source re-parses its file.

## What's cached

The rest of this page describes the cache's contents, for when you inspect a driver or write a plugin.

### Document bodies

Bodies are stored once parsed. They are keyed by `<name>:<path>`, where `<name>` is the [instance name](https://content.comark.dev/reference/content/comark-content#name) (for example `docs:guide.md`). Bodies populate lazily: the first `get()` for a path parses and stores it, and later reads return the stored copy until it's invalidated or expires.

### Raw bodies (expensive-read sources)

For sources flagged [`expensiveReads`](https://content.comark.dev/sources/custom#interface-expensivereads) (like [GitHub](https://content.comark.dev/sources/github)), the raw file bodies downloaded during a partial `init()` are kept under `<name>:__raw__:<path>`. The first full parse per file reads the cached body instead of re-fetching the source, then replaces it with the parsed entry. These keys never appear in `cache.keys()` and are invalidated alongside their document on `update()`/`remove()`.

### Index

The [manifest](https://content.comark.dev/reference/types/content#manifest) is the index of every entry by path, stored under a single `manifest` key. It is held in memory as the working copy and persisted to the cache, so with a shared or persistent driver a fresh instance restores it instead of re-reading the source:

1. On [`content.init()`](https://content.comark.dev/reference/content/init) the cache is checked for a stored manifest.
2. On a hit, it's restored into memory with no source walk.
3. On a miss, it's built from the source (reading each file's frontmatter) and persisted to the cache.
4. Mutations ([`update`](https://content.comark.dev/reference/content/update), [`remove`](https://content.comark.dev/reference/content/remove), [`refresh`](https://content.comark.dev/reference/content/refresh)) re-persist it with a newer timestamp.

### HTTP artifacts

The checksummed artifacts [`content.handler()`](https://content.comark.dev/reference/content/handler) serves live under `manifest-artifact` and `snapshot-artifact-<name>`. They are produced once per content change instead of once per request, and dropped whenever the content mutates.

### What the driver sees

The keys above are the ones you pass to `content.cache`. On the driver, every key is prefixed with the instance name (`docs:manifest`, `docs:manifest-artifact`), and on a [`withRef()`](https://content.comark.dev/reference/content/with-ref) instance with a `ref:` namespace as well. Two instances, or two refs of one instance, can share a driver without overwriting each other.


## Sitemap

See the full [sitemap](https://content.comark.dev/sitemap.md) for all pages.
