tiered TTL

Cache

Tiered blob cache built into the engine: L1 memory + L2 durable.

What it is

Cache on RedDB.

A tiered cache module purpose-built for the application caching workload Redis dominates today. L1 is a process-local memory shard with SIEVE / TinyLFU-style admission. L2 is a durable native cache store inside the database file.

Namespace + key + opaque bytes is the surface. TTL is per-entry, prefix and tag invalidation are first-class, and a Bloom filter sits between L1 and L2 so cache misses stay cheap.

If you came in for Redis-style ephemeral caching, this is the matching module — durability and the rest of RedDB's data models come along for free.

Code

Write it. Read it. Same engine.

Insert

CACHE PUT sessions 'abc123' '<bytes>' WITH TTL 3600s;

Query

CACHE GET sessions 'abc123';
CACHE INVALIDATE PREFIX sessions 'org:42:';

Driver examples

Connect from your language.

Real driver code — copy-paste ready, no pseudocode.

import { Client } from '@reddb/client'

const db = new Client({ url: process.env.REDDB_URL })

// Store a value with a 1-hour TTL
await db.query(
  "CACHE PUT 'session:abc123' = $1 TTL 3600",
  [JSON.stringify({ userId: 42, role: 'admin' })],
)

// Read it back — returns null on miss
const { rows } = await db.query("CACHE GET 'session:abc123'")
const session = rows[0]?.value ? JSON.parse(rows[0].value) : null

// Invalidate by tag
await db.query("CACHE INVALIDATE TAGS $1", [['user:42']])

Use cases

Where cache earn their place.

  • Session stores

    User sessions with TTL, fast lookup on every request, durable across restarts.

  • API response cache

    Memoised expensive third-party API responses keyed by request signature.

  • Render caches

    Computed page fragments / personalisation slots invalidated by tag when the source changes.

  • Migration off Redis

    Replace a Redis cluster while keeping the cache miss / invalidation contract Redis users expect.

Build it

End-to-end walkthrough.

Four steps from empty database to a real cache workload — each step shows the exact code and explains what the engine is doing.

  1. Step 1

    1. Create a namespace

    Namespaces let you size and configure each cache workload separately. L1 is process-local memory (SIEVE / TinyLFU admission); L2 is durable in the database file. The DEFAULT TTL applies when a PUT does not specify one.

    CREATE CACHE sessions
      L1 BYTES 256MB
      L2 BYTES 4GB
      DEFAULT TTL 1h;
  2. Step 2

    2. Put + get

    Tags are first-class. A session blob can be tagged by both user:42 and org:7 so it can be invalidated by either dimension later. Bytes are opaque — the engine never parses them.

    CACHE PUT sessions 'abc123' '<session-blob-bytes>'
      WITH TTL 3600s
      TAGS ['user:42', 'org:7'];
    
    CACHE GET sessions 'abc123';
    -- returns the bytes if present + not expired,
    -- otherwise returns null and increments the miss counter.
  3. Step 3

    3. Invalidation by prefix or tag

    Two invalidation surfaces for two patterns. Prefix is great when the key carries the dimension (org:42:abc123); tags are great when the dimension is orthogonal to the key. Both fire in O(matching) time.

    -- Bulk invalidate every session under org 42.
    CACHE INVALIDATE PREFIX sessions 'org:42:';
    
    -- Or invalidate everything tagged 'user:42'.
    CACHE INVALIDATE TAGS ['user:42'] FROM sessions;
  4. Step 4

    4. Read-through helper

    The Node / Python / Rust drivers ship a read-through helper so you write the cache pattern once instead of in every callsite. Cache hit is one round trip; miss is two.

    -- App-side pseudocode
    let cached = await db.cacheGet('sessions', sessionId)
    if (cached) return cached
    let computed = await rebuildSession(sessionId)
    await db.cachePut('sessions', sessionId, computed,
      { ttlMs: 3600_000, tags: [`user:${userId}`] })
    return computed

Engine features

What RedDB adds on top of cache.

The engine knobs you would otherwise wire up yourself — TTL, encryption, indexes, atomic ops, eventual consistency — applied to this data model.

  • Tiered L1 (memory) + L2 (durable)

    L1 is process-local memory with SIEVE/TinyLFU admission. L2 is durable shard inside the database file. Hot keys stay in L1; cold keys live in L2 — restarts do not flush the working set.

  • TTL per entry + namespace default

    CACHE PUT ... WITH TTL <ms> is per-entry. CREATE CACHE ... DEFAULT TTL sets the namespace fallback. Entries without explicit TTL inherit the namespace value.

  • Bloom filter for misses

    A space-efficient membership filter sits between L1 and L2. Negative answers skip the L2 lookup — cache misses stay cheap even at scale.

  • Tag-based invalidation

    CACHE PUT … TAGS [...] attaches arbitrary tags. CACHE INVALIDATE TAGS [...] fans out across L1 + L2 in one call. Lets you invalidate by orthogonal dimensions (user:42, org:7) without prefix gymnastics.

  • Prefix invalidation

    CACHE INVALIDATE PREFIX namespace 'org:42:' drops every key under that prefix. O(matching), not O(total). Pairs with prefixed-key conventions.

  • Read-through driver helper

    Every official driver (TS / Python / Rust / Go) ships a readThrough(namespace, key, builder) helper — single round trip on hit, two on miss. The miss path is in your driver, not in 30 callsites.

  • Same backup story as the rest of the engine

    Cache lives in the .rdb file. Snapshot + WAL archive cover it just like tables. No separate Redis backup pipeline to operate.

  • Per-namespace policies

    Policies attach per cache namespace. A worker role can read+write sessions but only read feature_flags. Tenant isolation works the same way — namespace per tenant + a policy per principal.

  • PITR via WAL archive

    Because cache shares the WAL, point-in-time recovery is free — restore the database to a wall-clock timestamp and the cache state at that timestamp comes back too. Sessions survive a botched deploy rollback.

Showcases

What cache can do once you bring the rest of the engine in.

Cross-model correlation, algorithm flex, multi-model patterns — things that take a stack of services elsewhere.

TTLs and quotas configured at runtime via KV

Cache-namespace defaults read $config.<path> so you tune them without redeploys.

A cache namespace reads its default TTL and capacity hints from KV at runtime. Operations team adjusts a single key during an incident — every CACHE PUT after that picks up the new value. No app rebuild, no Helm chart edit, no deploy.

sql

-- Knobs live in KV (engine config namespace).
SET CONFIG acme.cache.sessions.defaultTtlMs = 3600000;
SET CONFIG acme.cache.sessions.maxBytes     = 268435456;

-- Cache writes pull TTL from config.
CACHE PUT sessions $session_id $blob
  WITH TTL $config.acme.cache.sessions.defaultTtlMs ms
  TAGS [$user_tag];

-- Tune during an incident. No rebuild.
SET CONFIG acme.cache.sessions.defaultTtlMs = 600000;

Cache invalidation tied to source data writes

Update a row in users and drop the matching cache entries in the same transaction.

Bundle the row update and the CACHE INVALIDATE TAGS calls inside a single transaction. They commit together or roll back together — no "I changed the user but the cached session still says old name" window, and no application-tier coordinator to keep in sync.

sql

BEGIN;

UPDATE users
   SET email = $1
 WHERE id = 42;

CACHE INVALIDATE TAGS sessions ['user:42'];
CACHE INVALIDATE TAGS render_fragments ['user:42'];

COMMIT;

Per-tenant cache namespaces with quota

Each customer gets a sized cache, isolated by namespace, billed per-tenant.

A multi-tenant SaaS gives every customer 64MB of L1 cache. Namespacing keeps blast radius scoped: a noisy tenant can't evict another tenant's hot data. The meta->>'org' filter on a periodic stats query produces per-tenant billing.

sql

-- Provision per tenant.
CREATE CACHE org_42 L1 BYTES 64MB L2 BYTES 1GB DEFAULT TTL 1h;

-- Usage stats per tenant for billing.
SELECT
  namespace,
  hits,
  misses,
  bytes_used_l1,
  bytes_used_l2,
  evictions
FROM CACHE_STATS
WHERE namespace LIKE 'org_%'
ORDER BY bytes_used_l2 DESC;

Migrate from

Bring cache over from your current tool.

From

Redis cache

Most Redis cache patterns map 1:1 (GET / SET / SETEX / DEL / EXPIRE / EXISTS). The migration guide in our docs walks through each.

# Side-by-side dual-write during migration.
red migrate cache --from "redis://prod:6379" \
  --to "reds://admin@.../my-db:5050" \
  --namespace sessions \
  --pattern "session:*" \
  --strip-prefix "session:"

From

Memcached

Walk the slabs, replay every entry as a PUT with the original TTL. Keys without TTL inherit the namespace default.

red migrate cache --from "memcached://prod:11211" \
  --to   "reds://admin@.../my-db:5050" \
  --namespace api_responses

Recipes

Common patterns, real driver code.

typescript

Read-through helper

Idiomatic cache pattern in 6 lines — the driver hides the miss path.

async function getSession(id: string) {
  return db.cache.readThrough('sessions', id, async () => {
    const session = await rebuildSession(id)
    return { value: session, ttlMs: 3600_000, tags: [`user:${session.userId}`] }
  })
}

sql

Tag-based invalidation

When a user changes their password, kill every session tagged with their user id — one statement, fans out across L1 + L2.

CACHE INVALIDATE TAGS ['user:42'] FROM sessions;

sql

Namespace per workload

Size each cache for its access pattern — sessions are large + long-lived, render fragments are small + short-lived.

CREATE CACHE sessions       L1 BYTES 256MB L2 BYTES 4GB DEFAULT TTL 1h;
CREATE CACHE render_fragments L1 BYTES 64MB  L2 BYTES 1GB DEFAULT TTL 5m;
CREATE CACHE api_responses    L1 BYTES 128MB L2 BYTES 2GB DEFAULT TTL 30s;

Limits

What it does. What it costs.

Operational caps and durability semantics, no hand-waving.

Max value size
~10 MB per entry (configurable per namespace)
L1 hit latency
Single-digit microseconds (in-process)
L2 hit latency
Sub-millisecond (durable shard)
Eviction
TTL + SIEVE/TinyLFU admission, scan-resistant
Invalidation
Key, prefix, tag, dependency — all O(matching)

When NOT to use

Don't pick cache for these.

Honest constraints — when another model fits better.

  • Don't use Cache as the source of truth

    Cache evicts under pressure. State you can't reconstruct (auth tokens, billing state, user data) belongs in tables / KV with explicit durability semantics.

  • Don't cache results that change every request

    A 30-second TTL on a per-request response is hot path overhead, not optimisation. Profile first; cache where the same answer goes to many requests.

  • Avoid pub/sub patterns through cache TTL hacks

    Cache is not a message bus. For cross-service notifications use the queue primitive or KV's WATCH stream.

vs

How RedDB cache compare.

Redis

Same hot-path latency for L1 hits, plus durability and one-engine ops. No sync between cache and the database it caches.

Memcached

Tag and prefix invalidation are first-class. Multi-tenancy via namespace is native.

FAQ

Questions teams ask before they pick cache.

How does this differ from Key-Value?

KV is durable state with no eviction — feature flags, idempotency keys, counters. Cache is opaque blobs with TTL, prefix and tag invalidation, tiered L1+L2 storage. Different shape, different guarantees.

What is the L1 hit latency?

Single-digit microseconds for in-process L1 hits — no network, no parse, no plan. Compares to Redis localhost rates without a separate process.

Can I migrate from Redis without rewriting my app?

The most common Redis cache patterns (GET / SET / SETEX / DEL / EXPIRE / EXISTS / KEYS-by-prefix) map 1:1. Lua, MULTI/EXEC, pub/sub, sorted sets, and streams are NOT in scope — those stay on Redis if you depend on them.

How does the L1 admission policy decide what stays?

SIEVE / TinyLFU-style admission tracks frequency cheaply. Hot keys stay in L1; cold keys live in L2 (durable). Scan-resistant — a one-shot batch read does not flush the working set.

Is this safe to use as a session store?

Yes. L2 is durable so a process restart does not log everyone out. TTL eviction handles cleanup. Tag invalidation lets you forcibly log out one user / one org without touching the rest.

Try cache on RedDB.

Free nano database, no credit card. AGPL self-host or managed Cloud — same engine, same file format.