Cache
Production-grade response caching with TTL, SWR, request deduplication, and pluggable backends.
Cache query results in memory, Redis, Cloudflare KV, or any storage backend. Powered by ocache with built-in TTL, stale-while-revalidate, and request deduplication.
Basic usage
Wrap a query with cacheQuery() to cache its results:
import { } from 'silgi/cache'
const = s.$use(({ : 60 })).$resolve(({ }) => .db.users.findMany())The first call runs the resolver and caches the result. Subsequent calls with the same input return the cached value for 60 seconds.
Options
cacheQuery({
: 60, // TTL in seconds (default: 60)
: true, // stale-while-revalidate (default: true)
: 60, // max stale age in seconds (default: maxAge)
: 'users_list', // cache key prefix (default: auto-generated)
: () => .id, // custom cache key from input
})| Option | Type | Default | Description |
|---|---|---|---|
maxAge | number | 60 | Cache TTL in seconds |
swr | boolean | true | Return stale data while revalidating in the background |
staleMaxAge | number | maxAge | Maximum seconds to serve stale data during SWR |
name | string | auto | Cache key prefix for invalidation |
getKey | (input) => string | hash | Custom cache key generator |
shouldBypassCache | (input) => boolean | - | Skip cache entirely (e.g. admin, debug) |
shouldInvalidateCache | (input) => boolean | - | Force re-resolve and update cache |
validate | (entry) => boolean | - | Return false to treat entry as miss |
transform | (entry) => T | - | Transform cached value before returning |
base | string | string[] | '/cache' | Multi-tier storage prefixes |
integrity | string | auto | Custom integrity hash (auto-invalidates on redeploy) |
onError | (error) => void | console.error | Error handler for cache read/write failures |
Bypass and conditional invalidation
Skip the cache entirely for specific requests, or force a refresh:
import { } from 'silgi/cache'
const = k
.$use(({
: 300,
// Admin users bypass cache
: () => ( as any)?.asAdmin === true,
// Refresh flag forces re-resolve
: () => ( as any)?.refresh === true,
// Only cache non-empty arrays
: () => .(.) && .. > 0,
}))
.$resolve(({ }) => .db.users.findMany())
## Invalidation
Invalidate cached queries by — typically after a mutation:
```ts twoslash
import { cacheQuery, invalidateQueryCache } from 'silgi/cache'
const listUsers = k
.$use(cacheQuery({ maxAge: 300, name: 'users_list' }))
.$resolve(({ ctx }) => ctx.db.users.findMany())
const createUser = s.$resolve(async ({ input, ctx }) => {
const user = await ctx.db.users.create(input)
await invalidateQueryCache('users_list') // clear the cache
return user
})Storage backends
By default, cache lives in memory. For production, plug in any storage backend via unstorage.
bash pnpm add unstorage bash npm install unstorage bash bun add unstorage Redis
import { , } from 'silgi/cache'
import { } from 'unstorage'
import from 'unstorage/drivers/redis'
const = ({
: ({ : 'redis://localhost:6379' }),
})
(())Cloudflare KV
import { , } from 'silgi/cache'
import { } from 'unstorage'
import from 'unstorage/drivers/cloudflare-kv-binding'
const = ({
: ({ : 'MY_KV' }),
})
(())Custom storage
Any object with get and set methods works:
import { } from 'silgi/cache'
({
: () => myStore.get(),
: (, , ) => myStore.set(, , ?.),
})How it works
cacheQuery()wraps the procedure as onion middleware- On each call, input is hashed to produce a cache key
- Cache hit → return immediately (zero resolver overhead)
- Cache miss → run resolver, store result, return
- With SWR → return stale data immediately, revalidate in background
- Concurrent identical requests share one in-flight promise (deduplication)
- Cache entries auto-expire after
maxAgeseconds
The cache key is computed from the serialized input using ohash. Two calls with the same input always produce the same key, regardless of property order.
What's next?
- Rate Limiting — protect endpoints from abuse
- Server — HTTP-level
Cache-Controlheaders viaroute.cache - Plugins — all available plugins