Caching
Built-in caching for read queries with automatic invalidation and stale-while-revalidate support
Features
- Multiple Backends - In-memory, Cloudflare KV, or custom implementations
- Automatic Invalidation - Optionally clear cache when data changes
- Stale-While-Revalidate - Return stale data instantly while refreshing in background
- Serverless Ready - Works with Cloudflare Workers and Vercel Edge
- Type-Safe - Full TypeScript support, only read operations available on cached client
- Cache Versioning - Invalidate all cache on schema changes
- OpenTelemetry - Built-in tracing with hit/miss/stale status
Quick Start
import { createClient } from "viborm";
import { MemoryCache } from "viborm/cache/memory";
const client = createClient({
schema: { user, post },
driver,
cache: new MemoryCache(),
});
// Use cached queries
const users = await client.$withCache().user.findMany({
where: { active: true },
});
Configuration
Setting Up a Cache Driver
Pass a cache driver when creating the client:
import { MemoryCache } from "viborm/cache/memory";
import { CloudflareKVCache } from "viborm/cache/cloudflare-kv";
// In-memory cache (development/single instance)
const client = createClient({
schema,
driver,
cache: new MemoryCache(),
});
// Cloudflare KV cache (production/distributed)
const client = createClient({
schema,
driver,
cache: new CloudflareKVCache(env.MY_KV_NAMESPACE),
});
Cache Versioning
Use cacheVersion to invalidate all cached data when your schema changes:
const client = createClient({
schema,
driver,
cache: new MemoryCache(),
cacheVersion: 2, // Bump when schema changes
});
Cache keys become versioned: viborm:v2:user:findMany:...
When you update the version, all previously cached entries are automatically ignored (different key prefix). This prevents stale data with incompatible shapes from being returned after schema migrations.
Using $withCache
The $withCache() method returns a cached client that only exposes read operations:
// Default: 5 minute TTL
const cached = client.$withCache();
// Custom TTL
const cached = client.$withCache({ ttl: "1 hour" });
const cached = client.$withCache({ ttl: 30000 }); // 30 seconds in ms
// With stale-while-revalidate
const cached = client.$withCache({ ttl: "5 minutes", swr: true });
// Force fresh fetch (bypass cache read)
const fresh = client.$withCache({ bypass: true });
Available Options
| Option | Type | Default | Description |
|---|---|---|---|
ttl |
string | number |
300000 (5 min) |
Cache duration. String format: “1 hour”, “30 seconds”, etc. |
swr |
boolean | string | number |
false |
Enable stale-while-revalidate. When true, the stale window equals the TTL (entries stored for 2x TTL). A duration sets a custom stale window (entries stored for TTL + window). |
key |
string |
auto-generated | Custom cache key (overrides automatic key generation) |
bypass |
boolean |
false |
Skip cache read and force fresh fetch (still writes to cache) |
TTL Format
TTL can be specified as milliseconds or human-readable strings:
// Milliseconds
{ ttl: 60000 } // 1 minute
// Human-readable strings
{ ttl: "30 seconds" }
{ ttl: "5 minutes" }
{ ttl: "1 hour" }
{ ttl: "2 hours" }
{ ttl: "1 day" }
{ ttl: "1 week" }
Supported units: ms, s/sec/second/seconds, m/min/minute/minutes, h/hr/hour/hours, d/day/days, w/week/weeks, month/months
Cached Operations
Only read operations are available on the cached client:
const cached = client.$withCache({ ttl: "1 hour" });
// Available operations
await cached.user.findMany({ where: { active: true } });
await cached.user.findFirst({ where: { role: "admin" } });
await cached.user.findUnique({ where: { id: "123" } });
await cached.user.findUniqueOrThrow({ where: { id: "123" } });
await cached.user.findFirstOrThrow({ where: { role: "admin" } });
await cached.user.count({ where: { active: true } });
await cached.user.aggregate({ _avg: { age: true } });
await cached.user.groupBy({ by: ["role"], _count: true });
await cached.user.exist({ where: { email: "test@example.com" } });
// Mutation operations are NOT available on cached client
// cached.user.create() // TypeScript error!
// cached.user.update() // TypeScript error!
Stale-While-Revalidate (SWR)
SWR improves perceived performance by returning cached data immediately while refreshing in the background:
const cached = client.$withCache({ ttl: "5 minutes", swr: true });
// First request: cache miss, executes query
const users1 = await cached.user.findMany(); // ~50ms (database query)
// Second request (within TTL): fresh cache hit
const users2 = await cached.user.findMany(); // ~1ms (from cache)
// Third request (after TTL but within 2x TTL): stale hit + background refresh
const users3 = await cached.user.findMany(); // ~1ms (stale data returned)
// Background: query executes and updates cache
How SWR Works
- Fresh hit (age < TTL): Return cached data immediately
- Stale hit (TTL < age < SWR window): Return stale data, refresh in background
- Miss (age > SWR window or not cached): Execute query, cache result
With swr: true, the stale window equals the TTL, so entries are stored for 2× the TTL. A custom duration sets the stale window explicitly, so entries are stored for TTL + window:
// Default: stale window equals the TTL (stored for 10 minutes total)
const cached = client.$withCache({ ttl: "5 minutes", swr: true });
// Custom stale window of 1 hour (stored for 5 min + 1 hour = 65 minutes total)
const cached = client.$withCache({ ttl: "5 minutes", swr: "1 hour" });
// Custom stale window in milliseconds
const cached = client.$withCache({ ttl: 300000, swr: 3600000 });
Serverless Environments
In serverless environments (Cloudflare Workers, Vercel Edge), configure waitUntil at the client level to ensure background revalidation completes:
// Cloudflare Workers
export default {
async fetch(request, env, ctx) {
const client = createClient({
schema: { user },
driver: createD1Driver(env.DB),
cache: new CloudflareKVCache(env.CACHE),
waitUntil: ctx.waitUntil.bind(ctx),
});
const users = await client
.$withCache({ ttl: "5 minutes", swr: true })
.user.findMany();
return Response.json(users);
}
}
// Vercel Edge
import { waitUntil } from "@vercel/functions";
const client = createClient({
schema: { user },
driver,
cache: new MemoryCache(),
waitUntil,
});
export async function GET() {
const users = await client
.$withCache({ ttl: "5 minutes", swr: true })
.user.findMany();
return Response.json(users);
}
Cache Invalidation
Direct Invalidation with $invalidate
Use $invalidate to manually invalidate cache entries at any time:
// Invalidate specific keys
await client.$invalidate("user:findMany:abc123");
// Invalidate by prefix (use * suffix)
await client.$invalidate("user:*");
// Invalidate multiple patterns
await client.$invalidate("user:*", "post:findMany:*");
Invalidation on Mutations
Specify cache keys or prefixes to invalidate when performing mutations:
// Invalidate specific keys
await client.user.update({
where: { id: "123" },
data: { name: "Alice" },
cache: {
invalidate: ["user:findUnique:abc123"],
},
});
// Invalidate by prefix (note the * suffix)
await client.user.update({
where: { id: "123" },
data: { name: "Alice" },
cache: {
invalidate: ["user:findMany:*"], // Clears all findMany caches for user
},
});
// Invalidate multiple patterns
await client.post.create({
data: { title: "New Post", authorId: "123" },
cache: {
invalidate: [
"post:*", // All post caches
"user:123:*", // All caches for this user
],
},
});
Automatic Model Invalidation
Enable autoInvalidate to automatically clear all cache entries for a model after mutations:
await client.user.update({
where: { id: "123" },
data: { name: "Alice" },
cache: {
autoInvalidate: true, // Clears all user:* cache entries
},
});
Cache Key Generation
Cache keys are automatically generated from the model name, operation, and query arguments:
viborm[:v<version>]:<model>:<operation>:<hash>
For example:
viborm:user:findMany:a1b2c3d4e5f6...
viborm:v2:user:findMany:a1b2c3d4e5f6... // with cacheVersion: 2
The hash is deterministic — identical queries always produce the same key, regardless of object property order.
Custom Cache Keys
Override automatic key generation when needed:
const cached = client.$withCache({
ttl: "1 hour",
key: "homepage-featured-users",
});
const users = await cached.user.findMany({
where: { featured: true },
take: 5,
});
Generating Keys for Manual Invalidation
Use generateCacheKey to create keys matching the auto-generated format:
import { generateCacheKey } from "viborm/cache";
// Generate the same key that would be used for a cached query
const key = generateCacheKey(
"user",
"findMany",
{ where: { active: true } },
2 // optional: cacheVersion
);
// "viborm:v2:user:findMany:abc123..."
// Use for precise invalidation
await client.$invalidate(key);
Observability
Cache operations emit OpenTelemetry spans when instrumentation is configured.
Spans
| Span Name | Description |
|---|---|
viborm.cache.get |
Cache read operation |
viborm.cache.set |
Cache write operation |
viborm.cache.delete |
Cache key deletion |
viborm.cache.clear |
Cache prefix clearing |
viborm.cache.invalidate |
Cache invalidation |
Attributes
| Attribute | Description |
|---|---|
cache.driver |
Driver name (memory, cloudflare-kv, etc.) |
cache.result |
Result of cache read: hit, miss, stale, or bypass |
cache.ttl |
TTL in milliseconds |
Cache keys, including custom keys, are omitted from spans and logs — see Privacy & Security.
Example Trace
viborm.operation (user.findMany)
└── viborm.cache.get (cache.result: "hit")
When SWR returns stale data:
viborm.operation (user.findMany)
└── viborm.cache.get (cache.result: "stale")
└── [background] viborm.cache.set
Cache Drivers
VibORM includes built-in cache drivers and supports custom implementations.
| Driver | Best For | Persistence |
|---|---|---|
| MemoryCache | Development, testing | No |
| CloudflareKVCache | Cloudflare Workers | Yes |
| Custom | Redis, Upstash, etc. | Varies |
See Cache Drivers for detailed configuration and implementation guides.