Stop your database from taking every hit.
One call wraps your query. cachemate busts the cache the instant your tables change and keeps serving from memory the moment Redis goes down. Your database never feels the difference.
const client = new Client({ memoryMaxMb: 64, redisUrls: [redisUrl] });
const user = await client.cache({
tables: ["users"],
filters: { id: userId },
loader: () => db.users.findById(userId),
});
// one write, every cached read for "users" invalidates
await client.invalidate("users");Manual cache invalidation is a bug you write on purpose.
Every team that wires up Redis by hand eventually misses one write path. The cache goes stale and nobody notices until a support ticket does. cachemate keys every cached read to the tables it reads from, so one write invalidates everything that depends on it.
Five things a hand-rolled Redis wrapper never gets right.
Table-based auto invalidation
Register a table once. Every read for it carries that version in its key, so one write anywhere in your app invalidates every cached query that touched it.
registry: {
entries: {
orders: { tag: "orders", versionKey: "v:orders" },
},
},O(1) row lookups
rows() caches ids and row bodies separately, so a partial cache hit only refetches the ids that actually missed, not the whole list.
Redis to memory failover
Every node goes down, cachemate falls through to an in-process LRU cache. Reads and writes keep returning, nothing throws.
Postgres LISTEN / NOTIFY
Optional pg-listen integration wires a Postgres trigger straight to invalidate(), zero app-code cache busting.
Multi-node Redis pool
Pass more than one Redis URL and cachemate pools across them, advancing to the next healthy node on failure or OOM.
Three moves. Zero cache-busting code.
- 01
Wrap your query
Call client.cache() with the tables it touches, your filters, and the loader function you already have.
- 02
cachemate builds the key
It hashes your tables, filters, and each table's current version into one cache key. Same query, same key, every time.
- 03
You write, one counter moves
invalidate("orders") runs a single Redis INCR. The version changes, and every cached read for that table is a miss on its next call.
Same cache. One fewer place for it to go wrong.
Hand-rolled Redis
const key = `user:${id}`;
let user = await redis.get(key);
if (!user) {
user = await db.users.findById(id);
await redis.set(key, JSON.stringify(user), "EX", 60);
}
// elsewhere, on update:
await redis.del(`user:${id}`);
// forgot: user:list:*, user:${id}:orders...With cachemate
const user = await client.cache({
tables: ["users"],
filters: { id },
loader: () => db.users.findById(id),
});
// one call, invalidates every cached
// read that touched "users"
await client.invalidate("users");Protect your database, not just your response times.
A cache that goes down and takes your app with it is worse than no cache. If every Redis node in the pool is unreachable or reports OOM, cachemate advances to the next node automatically, and if none are left, it serves from an in-process LRU cache instead. Your database never sees the traffic spike a dead cache would have sent it.
npm install to first cache hit. About a minute.
npm install cachemateimport { Client } from "cachemate";
const client = new Client({
memoryMaxMb: 64,
redisUrls: [process.env.REDIS_URL],
registry: {
entries: {
orders: { tag: "orders", versionKey: "v:orders" },
},
},
});
await client.ready();Looking for LISTEN / NOTIFY triggers, custom key hashing, or multi-node pool options?
Read the docsFrequently asked, quickly answered.
Any Redis-compatible endpoint works: self-hosted, Redis Cloud, Upstash, ElastiCache. Pass one or more connection URLs and cachemate pools across them.