Skip to content
VibORM
Esc
navigateopen⌘Jpreview
On this page

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

  1. Fresh hit (age < TTL): Return cached data immediately
  2. Stale hit (TTL < age < SWR window): Return stale data, refresh in background
  3. 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.

Was this page helpful?