---
title: 'Cache'
slug: cache
description: 'Cache server-side data with stale-while-revalidate semantics using MochiCache.'
---
## Cache
`MochiCache` caches server-side data — typically slow upstream API calls — with stale-while-revalidate semantics. Construct once at module scope and share the instance across requests.
```ts
// src/lib/cache.ts
import { MochiCache } from 'mochi-framework';
export const pokemonCache = new MochiCache({
minTimeToStale: 10_000, // serve fresh for 10s
maxTimeToLive: 300_000, // hard expiry at 5min
});
```
Use it from a page or API route:
```svelte
```
**Caches are shared in-process.** Every request reads from the same `Map`, so a key like `cart:current` will leak one user's data to another. Prefix per-user keys with the user id — e.g. `` `cart:${userId}` `` — and do the same for any other request-scoped dimension (tenant, locale, role).
### Behaviour
- **Fresh** (within `minTimeToStale`): cached value returned, no fetch.
- **Stale** (between `minTimeToStale` and `maxTimeToLive`): cached value returned immediately, fetch runs in the background and updates the cache.
- **Expired** (past `maxTimeToLive`): fetch runs synchronously and the caller waits.
### API
| Method | Returns |
| -------------------------- | ---------------------------- |
| `fetch(key, fn)` | `Promise` |
| `fetchWithStatus(key, fn)` | `Promise<{ value, status }>` |
| `delete(key)` | `Promise` |
| `clearItems()` | `Promise` |
`clearItems()` empties the whole cache in one call.
`status` is `'fresh' \| 'stale' \| 'expired' \| 'miss'`.
### Options
| Option | Default |
| ---------------- | ----------------- |
| `minTimeToStale` | `5_000` (5s) |
| `maxTimeToLive` | `600_000` (10min) |
| `storage` | in-memory `Map` |
| `serialize` | identity |
| `deserialize` | identity |
For multi-process or persistent caching, pass a custom `storage` that implements `getItem` / `setItem` / `removeItem` / `clear` (e.g. Redis, SQLite via `bun:sqlite`). These methods may be synchronous (in-memory `Map`, `bun:sqlite`) or `async` / Promise-returning (Redis, network stores) — the cache awaits every call. Each key holds a single entry (the value plus its write time). When a backend needs a string or buffer — like Redis — supply `serialize` / `deserialize` to encode and decode that entry, e.g. `serialize: JSON.stringify, deserialize: JSON.parse`.
**In-flight de-duplication is per-server.** Concurrent calls for the same key on one instance share a single `fn` invocation, but that coordination lives in process memory. With multiple instances behind a shared backend (`bun:sqlite`, Redis), each instance de-duplicates only its own requests — so on a cold key, every instance may run `fn` once and race to write the same entry. The shared store keeps results consistent; it does not collapse the concurrent fetches into one.
If a `storage` call throws, the cache degrades instead of failing the request: a read error recomputes via `fn` (reported as a `miss`), a write error returns the freshly computed value uncached, and a `delete` error is re-thrown to the caller. Every case also emits a `cache:error` event.
### Subscribing to cache events
`MochiCache` emits these events on `mochiEvents`:
| Event | Payload | When |
| ------------------------- | --------------------------- | ------------------------------------------------------------ |
| `cache:read` | `{ key, status }` | Every cache lookup, regardless of which method ran. |
| `cache:revalidate` | `{ key }` | A background refetch starts (stale read). |
| `cache:revalidate:failed` | `{ key, error }` | A background refetch threw; the stale value is still served. |
| `cache:error` | `{ key, operation, error }` | A `storage` `get` / `set` / `remove` call threw. |
`consoleLogger()` surfaces `cache:revalidate:failed` and `cache:error` as warnings — a silently degrading upstream or storage backend is otherwise invisible.
`status` is `'fresh' \| 'stale' \| 'expired' \| 'miss'`. Use `mochiEvents.setHandler` to attach a custom subscriber — it replaces a prior handler under the same name, so dev re-imports don't pile up listeners:
```ts
import { mochiEvents } from 'mochi-framework';
mochiEvents.setHandler('metrics:cache-read', 'cache:read', ({ key, status }) => {
metrics.increment(`cache.${status}`, { key });
});
```
`consoleLogger()` already prints `cache:revalidate` lines by default. Pass `{ cache: 'verbose' }` to also print every read, or `{ cache: false }` to silence cache logging:
```ts
import { consoleLogger } from 'mochi-framework';
consoleLogger({ cache: 'verbose' });
```
See the [Cache Events demo](/demos/cache-events/) for a worked example that pipes events into an in-memory ring buffer and renders them on the page.
### Server-only
`MochiCache` lives on the server. Importing it into a hydratable island throws — caches are shared per-process state and don't make sense in the browser. Construct cache instances in `.ts` modules or page-route scripts, never inside a `mochi:hydrate` component.