Cache-aside with Redis in an Express API
A practical walkthrough of the cache-aside pattern: what to cache, how to key it, when to invalidate, and the mistakes that quietly serve stale data.
Md. Shafikul Islam
3 min read
On this page
Most read-heavy APIs spend their time asking the database the same question over and over. A cache in front of those queries is one of the cheapest wins you can ship — but only if you get the boring details right: keys, expiry and invalidation.
This post walks through the cache-aside pattern with Redis and Express, the version I reach for first.
What cache-aside means
With cache-aside, the application owns the cache. Redis never talks to your database; your code does:
- Look for the value in Redis.
- On a miss, read from the database.
- Write the result to Redis with a TTL, then return it.
- When the underlying data changes, delete the cached key.
It is simple, it fails safe (if Redis is down you just hit the database), and it keeps the cache full of things people actually ask for.
A small helper
Wrap the pattern once so every route doesn't reinvent it:
import { createClient } from "redis";
export const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
export async function cached<T>(
key: string,
ttlSeconds: number,
load: () => Promise<T>,
): Promise<T> {
const hit = await redis.get(key);
if (hit) return JSON.parse(hit) as T;
const value = await load();
// NX avoids overwriting a fresher value written by a concurrent request.
await redis.set(key, JSON.stringify(value), { EX: ttlSeconds, NX: true });
return value;
}And use it in a route:
router.get("/tenants/:tenantId/employees/:id", async (req, res) => {
const { tenantId, id } = req.params;
const employee = await cached(
`t:${tenantId}:employee:${id}:v1`,
300,
() => db.employee.findFirst({ where: { id, tenantId } }),
);
if (!employee) return res.sendStatus(404);
res.json(employee);
});Keys are a design decision
A good cache key is specific, namespaced and versioned:
- Specific — include every input that changes the answer (tenant, id, filters, page).
- Namespaced — prefix by tenant in multi-tenant apps (
t:42:...) so one tenant can never read another's cached data. - Versioned — add a
:v1suffix. When the shape of the cached object changes, bump it to:v2and old entries simply age out.
If two different requests can produce the same key but different answers, you have a data leak waiting to happen.
Invalidation: delete, don't update
When data changes, delete the key instead of writing the new value into the cache:
await db.employee.update({ where: { id }, data });
await redis.del(`t:${tenantId}:employee:${id}:v1`);Deleting is idempotent and avoids races where an older write lands after a newer one. The next read repopulates the cache from the source of truth.
For lists (e.g. "all employees in a department") track the keys in a Redis set per entity, or keep list TTLs short and accept a little staleness.
Pick TTLs on purpose
| Data | Changes | Suggested TTL |
|---|---|---|
| Reference data (countries, plans) | Rarely | Hours |
| Profiles, settings | Sometimes | 5–15 minutes |
| Dashboards, counts | Often | 30–60 seconds |
| Balances, permissions | Must be exact | Don't cache, or invalidate on every write |
A TTL is your safety net for the invalidation you forgot.
Mistakes to avoid
- Caching
nullforever. Cache misses for unknown ids with a short TTL, or attackers can hammer your database with random ids. - Thundering herds. When a hot key expires, many requests miss at once. Add a little jitter to TTLs, or use a short lock so only one request reloads.
- Unbounded memory. Set
maxmemoryand an eviction policy such asallkeys-lruin Redis. - Forgetting the tenant. Every key in a multi-tenant system should start with the tenant id.
Wrap-up
Cache-aside is a few lines of code; the value is in the discipline around it. Name keys carefully, delete on write, choose TTLs deliberately — and measure your hit rate so you know it's actually working.
Written by
Md. Shafikul Islam
Full-Stack & AI Engineer
I build scalable SaaS platforms & AI-powered systems.