Next.js caching explained: the four layers and how to control them
Most 'Next.js is showing stale data' bugs are one of four caches doing exactly what it was told. Here is the mental model that makes them predictable.
Cache-aside is the default Redis pattern: the application checks the cache, falls back to the database on a miss, and writes the result back with a TTL. The failure mode to design for is the stampede — when a hot key expires and hundreds of requests hit the database simultaneously — solved with a lock, early recomputation, or jittered TTLs.
| Pattern | How it works | Trade-off |
|---|---|---|
| Cache-aside | App reads cache, falls back to DB, writes back | Simple; first request after expiry is slow |
| Read-through | Cache layer fetches on miss | Cleaner app code; needs a cache library |
| Write-through | Write to cache and DB together | Cache always warm; slower writes |
| Write-behind | Write to cache, flush to DB async | Fast writes; risk of data loss |
| Refresh-ahead | Recompute before expiry | No cold misses; wasted work on cold keys |
Cache-aside covers the vast majority of web workloads and keeps failure behaviour obvious: if Redis is down, you fall back to the database and get slower, not broken. Verify that fallback actually works — a cache that takes the site down when it fails is a liability.
app:v3:user:1042:profile # namespace : schema version : entity : id : field
app:v3:product:list:cat=shoes:p=2 # include every parameter that changes the result
app:v3:session:8f2c... # opaque ids for sessionsA stampede happens when a popular key expires and every concurrent request recomputes it at once, hitting the database with a burst it was shielded from.
async function getWithLock<T>(key: string, ttl: number, load: () => Promise<T>) {
const hit = await redis.get(key);
if (hit) return JSON.parse(hit) as T;
const lock = await redis.set(`${key}:lock`, "1", { NX: true, EX: 10 });
if (!lock) {
await sleep(50); // someone else is computing it
return getWithLock(key, ttl, load);
}
const value = await load();
const jitter = Math.floor(Math.random() * ttl * 0.1);
await redis.set(key, JSON.stringify(value), { EX: ttl + jitter });
await redis.del(`${key}:lock`);
return value;
}What it is not: a durable primary datastore. Configure persistence and eviction deliberately, know your maxmemory policy, and treat anything in Redis as reconstructable from a source of truth.
Match it to how stale the data may be and how expensive it is to recompute. Minutes for listing pages, seconds for prices, hours for reference data. Always add jitter.
It applies your maxmemory-policy — evicting least-recently-used keys, or rejecting writes if the policy is noeviction. Set this explicitly; the default may not be what a cache wants.
Whichever is more expensive and more reusable. Caching a rendered fragment saves rendering and querying; caching a query result is more reusable across pages. Many systems do both at different layers.
Command execution is effectively single-threaded, which is why a slow command like KEYS on a large keyspace blocks everything. Recent versions use threads for I/O but keep the execution model simple.
Harshal Patel
Founder & Lead Engineer, ROVQIX
Harshal leads engineering at ROVQIX, where he has shipped production Next.js, Node.js and PostgreSQL systems for startups, SaaS teams and ecommerce brands. He writes about the trade-offs behind architecture decisions rather than the framework of the week.
ROVQIXdesigns and builds production web platforms — Next.js front ends, Node.js APIs and the infrastructure behind them. Tell us what you're building and we'll scope it with you.
Most 'Next.js is showing stale data' bugs are one of four caches doing exactly what it was told. Here is the mental model that makes them predictable.
Rate limiting is not just abuse prevention. It is the mechanism that stops one client's bad afternoon from becoming everyone's outage.
Everyone runs EXPLAIN. Fewer people read the row estimates, which is where the actual answer usually is.
No spam. Just the occasional case study and craft breakdown.