For AI agents: the complete documentation index is available at https://dotpress.dev/llms.txt, the full documentation bundle is available at https://dotpress.dev/llms-full.txt, and this page is available as Markdown at https://dotpress.dev/reference/cache.md.

Cache

The options, stores and errors of @dotpress/cache. For a guide, see Cache and memoization.

defineMemo()

defineMemo({ name: 'user', load: (id: string) => db.users.findById(id) })
defineMemo({ name: 'author', batch: (ids: readonly string[]) => loadAuthors(ids), maxBatchSize: 100 })
OptionDescription
nameUsed in errors and logs.
loadLoads one value from its arguments.
batchInstead of load: loads the values of every key requested in the same tick, in one call.
keyOptional: a string from the arguments, for arguments not compared by value (key: (user) => user.id).
maxBatchSizeWith batch: the maximum number of keys per call.
scheduleWith batch: when the batch runs, setTimeout(run, 0) by default.
  • Keys: the arguments, compared by value: strings, numbers, booleans, null, undefined, bigints, dates, arrays and plain objects, whatever the order of their properties.
  • Single flight: calls made while a value loads share the load. A failed load fails the calls in progress, and is not kept: the next call loads again.
  • Values are the objects returned by load, not copies: a caller that mutates one changes it for the others.
  • batch receives the keys without duplicates, and returns a Map by key (missing keys give undefined), or an array in the order of the keys, where an Error fails its key only. An array of the wrong length fails every key with a MemoBatchError. Keys requested before the batch runs join it, awaits included; keys requested once it started go to the next one.

The memo

createMemo() creates a memo: one per request, event message or job, never one per process.

CallDescription
memo(loader, ...args)The value, loaded once.
memo.set(loader, ...args, value)Replaces a value, after an update.
memo.delete(loader, ...args)The next call loads again.
memo.clear(loader), memo.clear()Forgets the values of a loader, or every value.

defineCacheEntry()

defineCacheEntry({
  name: 'user',
  key: (id: string) => id,
  schema: z.object({ id: z.string(), name: z.string() }),
  ttl: '1h',
  tags: (id: string) => [`user:${id}`],
})
OptionDefaultDescription
namerequiredPart of the key.
keythe argumentsA string, a number, or a value compared like the arguments of a memo.
schema—Validates every value read, and every value written. Types the value.
version1Part of the key: increase it when the shape of the values changes.
tags—A function of the arguments, returning the tags of the value.
ttlthe ttl of the cacheHow long a value is fresh.
grace, graceBackoff, staleWhileRevalidate, timeout, hardTimeout, lockthe policy of the cacheSee Resilience.

Keys are prefix:name:vVersion:key, like app:user:v1:42. Keys longer than the store accepts are hashed (SHA-256). Durations are a number of seconds, or a string: '500ms', '30s', '10m', '1h', '7d'.

Without a schema, getOrSet returns the type of the loader, and get returns unknown: defineCacheEntry<[id: string], User>({ … }) declares the type.

createCache()

OptionDefaultDescription
storerequiredSee Stores.
prefix—Prefix of every key, like the name of the app.
ttl1 hourDefault lifetime of the values.
grace… lock—The default resilience policy of the entries.
serializerJSON{ serialize, deserialize }, such as superjson.
logger—Logs store failures, stale values and events.
storeTimeout—A slower store operation counts as a failure. Set it: a reconnecting client makes commands wait.
failOpentruefalse throws a CacheStoreError when the store fails.
onError(error, { operation, key, entry })—Every store failure; operation is get, set, delete or invalidate. Its own errors are logged and ignored.
onEvent(event)—Each result of get and getOrSet, for metrics. See Events.
MethodDescription
getOrSet(entry, ...args, loader)The value, or loads, caches and returns it. Concurrent calls share the load. A failed load, or a load returning undefined, is not cached.
get(entry, ...args)A fresh value, or undefined when missing, expired or invalidated.
set(entry, ...args, value)Writes a value.
delete(entry, ...args)Removes a value for good.
invalidateTags(tags)Expires every value of these tags.
withLogger(logger)The same cache, with another logger: the logger of a request.

Values

Values are serialized with the serializer. undefined is not cached; null is. With a schema, a value read that no longer matches (written by an older version of the app) is a miss, not a crash. Values are checked when written too, as they will read back: JSON turns dates into strings, so z.date() fails and the value is not cached, with a warning, while z.coerce.date() works. getOrSet returns the value as it reads back: a hit and a miss return the same thing. The memory store serializes too, so that a caller cannot change a cached object.

Resilience

OptionDefaultEffect, once the ttl is over
gracedisabledDuring this time, when the loader fails or times out, the expired value is returned instead of the error, with a warn log.
graceBackoff'10s'After such a failure, the loader is not called again during this delay: the expired value is served directly. A successful load ends it.
staleWhileRevalidatedisabledDuring this time, the expired value is returned at once, and reloaded in the background, once per key. Failed refreshes are logged.
timeoutnoneWith an expired value in its grace period, a slower load returns that value; the load goes on and caches its result.
hardTimeoutnoneA slower load fails with a CacheTimeoutError, or returns the expired value of the grace period. The load goes on and caches its result.
locknoneDistributed single flight, on stores with locks (Redis): one instance loads a missing value, under a lock held at most this long; the others read the value every 50 ms, and load it themselves if the lock was not released. Set it above the duration of the loader.

Values stay in the store for ttl plus the longer of grace and staleWhileRevalidate. Without a grace period, a failing loader fails the call.

Tags

Each tag has a random token in the store (prefix:_tag:name, kept 30 days or longer). A value records the tokens of its tags when written; invalidateTags() replaces them. Reading a value with tags reads their tokens too: one more round trip, grouped with getMany when the store has it.

  • A value whose token changed or vanished is expired: get returns undefined, and getOrSet reloads it. With a grace period, it can still be served when the loader fails, never by staleWhileRevalidate: the data changed.
  • When the tokens cannot be read, values with tags count as invalidated, and new values are not cached.

Events

onEvent(event) receives { type, reason?, entry, key }: type is hit, miss or stale, and reason (for a stale value) is revalidate, error, timeout or backoff. The same events are logged at the debug level.

Store failures

By default, the cache fails open: a failed read is a miss (the loader runs), a failed write is skipped. Every failure is logged, warn for reads and writes, error for deletions and invalidations (they may leave outdated values), and passed to onError.

Stores

StoreImportUse
createMemoryStore({ maxEntries })@dotpress/cacheThe memory of the process: one instance, development, tests. The least recently used entries are removed beyond maxEntries (10,000 by default).
createRedisStore({ client })@dotpress/cache/redisRedis or Valkey, with a node-redis or ioredis client.
createUpstashStore({ url, token })@dotpress/cache/upstashUpstash Redis over its REST API: every runtime, edge included.
createCloudflareKvStore({ kv })@dotpress/cache/cloudflare-kvWorkers KV, from a Worker.

Redis and Valkey

import { createClient } from 'redis' // or: import { Redis } from 'ioredis'
import { createRedisStore } from '@dotpress/cache/redis'

const client = createClient({ url: process.env.REDIS_URL })
client.on('error', (error) => logger.error({ error }, 'Redis error'))
await client.connect()

const cache = createCache({ store: createRedisStore({ client }), prefix: 'app', storeTimeout: '200ms' })
// On shutdown: await client.close() (node-redis 5 or later; quit() with node-redis 4 and ioredis)
  • The store uses the client of the app, which connects and closes it: node-redis (redis, v4 or later) or ioredis.
  • It works with Redis, Valkey, managed services (ElastiCache, Memorystore…) and servers of the same protocol (Dragonfly, KeyDB…).
  • Commands: GET, MGET (tags), SET … PX, DEL, and for lock, SET … NX PX and a Lua script that releases a lock with its token only.
  • Cluster clients (createCluster(), new Cluster()) are refused for now: use a standalone server, or the primary of a replicated one.
  • A Redis used as a cache only can evict the least recently used keys (maxmemory-policy allkeys-lru); a Redis shared with BullMQ needs noeviction.

Upstash

import { createUpstashStore } from '@dotpress/cache/upstash'

const store = createUpstashStore({
  url: env.UPSTASH_REDIS_REST_URL,
  token: env.UPSTASH_REDIS_REST_TOKEN, // the standard token: the read-only one cannot write
})
const cache = createCache({ store, prefix: 'app', storeTimeout: '500ms' })

The store calls the REST API of Upstash Redis with fetch: no dependency, no connection to open or close. It runs on Cloudflare Workers, Vercel, Deno, Bun and Node.js. Each operation is one HTTPS request. Errors of the API (401, 400 ERR …) are store failures. A request is limited to 10 MB on the free and pay-as-you-go plans. The fetch option replaces the global fetch.

The @upstash/redis SDK is not needed: by default, it applies JSON.parse to every reply, which would change the cached values.

Cloudflare Workers KV

// wrangler.jsonc
{ "kv_namespaces": [{ "binding": "CACHE", "id": "<namespace id>" }] }
import { env } from 'cloudflare:workers'
import { createCloudflareKvStore } from '@dotpress/cache/cloudflare-kv'

const cache = createCache({ store: createCloudflareKvStore({ kv: () => env.CACHE }), prefix: 'app' })

kv is the binding, or a function that returns it. Workers KV suits values read often and changed rarely: configuration, catalogs, rendered pages.

LimitEffect
Eventual consistencyA write, deletion or invalidation can take 60 seconds or more to reach other locations, which serve the previous value meanwhile.
Expiration of 60 seconds at leastShorter ttl are kept in the value: the cache expires them on time, KV removes them after 60 seconds.
One write per second per keyFaster writes fail with a 429: a store failure. invalidateTags() writes a token per tag.
Keys of 512 bytesLonger keys are hashed.
No locksThe lock option is ignored.
Free plan: 1,000 writes per dayCache misses write: count them before choosing KV.

cacheTtl (30 seconds at least) keeps the values read in the cache of the location for longer: faster reads, but changes are seen even later. The tokens of tags are read in bulk, 100 keys per read.

Custom stores

A store implements CacheStore: name, get(key), set(key, value, ttl) (strings, ttl in milliseconds) and delete(key), and optionally getMany(keys) (one round trip for the tokens of tags), lock(key, ttl) and unlock(key, token) (the lock option), and maxKeyLength (in bytes).

cachePlugin()

import { cachePlugin } from '@dotpress/cache/plugin'

await createApp({ adapter, plugins: [cachePlugin({ cache, memoScope: asyncLocalMemo })] })
OptionDescription
cacheOptional: provides ctx.cache, the cache with the request logger. Without it, using ctx.cache throws.
memoScopeOptional: asyncLocalMemo runs each request in the scope of its memo.

The plugin, named cache, provides ctx.memo: a new memo for each request, created on first use.

@dotpress/cache/als

With AsyncLocalStorage: Node.js, Bun, Deno, and Cloudflare Workers with the nodejs_compat flag.

ExportDescription
asyncLocalMemoThe memoScope of the plugin.
memoized(loader)A function that reads the memo of the current scope.
runWithMemo(fn)Runs fn in the scope of a new memo: one per event message or job.
currentMemo()The memo of the scope, for set and delete.

Outside a scope, currentMemo() and the memoized() functions throw an explicit error: there is no global memo.

Errors

ErrorWhen
TypeErrorInvalid options and arguments; the message names the option.
MemoBatchErrorbatch returned values that do not match the keys. Field: memo.
CacheTimeoutErrorA load exceeded hardTimeout, without a stale value to serve. Fields: entry, key, timeout.
CacheStoreErrorThe store failed or timed out, with failOpen: false. Fields: store, operation, key, cause. Also passed to onError.

The error classes extend CacheError.