If you self-host Next.js 15 on Docker, Kubernetes, Railway, Coolify, or Fly.io, you quickly run into two production caching challenges:
- Multi-Instance Cache Desync: By default, Next.js stores ISR and App Router
fetchcaches on the local container filesystem/memory, meaning Pod A and Pod B serve inconsistent data afterrevalidateTag(). - The ISR Cache Stampede (Thundering Herd): When a high-traffic cached route or query expires under 1,000 req/sec, dozens of concurrent requests trigger expensive regeneration queries against your database at the exact same millisecond.
To solve both problems with zero runtime dependencies in the core engine, we built KVDock — featuring a drop-in Next.js 15 Incremental Cache Handler, multi-tier L1/L2 caching, and single-flight stampede coalescing.
🎮 Launch the Live Browser Stampede Simulator — Simulate 5,000 concurrent cache misses in your browser!
⭐ Support open-source: Give KVDock a ★ Star on GitHub (We are just 13 stars away from our first 16-star community milestone!)
⚡ What Happens During a Cache Stampede?
Imagine a popular product page cache expires while 1,000 concurrent users load it simultaneously:
| Metric | Standard Uncoalesced Cache | kvdock.getOrSet() (Single-Flight) | Impact |
|---|---|---|---|
| Concurrent Requests | 1,000 | 1,000 | Identical load |
| Database Queries Executed | 1,000 queries (Thundering Herd) | 1 query (Single-Flight) | 99.9% DB load reduction |
| Coalesced Callers | 0 | 999 | Zero lock contention |
🛠️ 3-Line Next.js 15 Setup (next.config.mjs)
First, install kvdock:
npm install kvdock Wire up the custom cache handler in next.config.mjs:
/** @type {import("next").NextConfig} */ const nextConfig = { cacheHandler: import.meta.resolve("kvdock/next"), cacheMaxMemorySize: 0, // Let KVDock TieredDock manage L1 RAM + L2 Redis/SQLite }; export default nextConfig; 🚀 Multi-Tier L1 (In-Memory) + L2 (Redis / SQLite) Architecture
Why pay a 2–5ms Redis network round-trip on every single read when 95% of reads can be served in 0.04ms from L1 RAM?
With TieredDock, you get sub-millisecond local reads backed by shared L2 persistence:
import { TieredDock } from "kvdock"; import { MemoryAdapter } from "kvdock/adapters/memory"; import { RedisAdapter } from "kvdock/adapters/redis"; export const cache = new TieredDock({ l1: new MemoryAdapter({ maxEntries: 5_000 }), l2: new RedisAdapter({ url: process.env.REDIS_URL }), l1Ttl: 60_000, // 1 minute hot in-process L1 cache l2Ttl: 3_600_000, // 1 hour shared Redis L2 cache }); // Single-flight coalesced read-through: const catalog = await cache.getOrSet("catalog:featured", async () => { return await db.query("SELECT * FROM products WHERE featured = true"); }); When an L1 miss occurs:
-
TieredDockchecks L2 (Redis, SQLite, Upstash, Postgres, or S3). - If found in L2, it automatically promotes the value back into L1 RAM.
- If missing in both, Single-Flight Coalescing ensures only 1 caller queries your database while all other concurrent requests await the same in-flight promise.
🧪 Try the Interactive Playground & Star on GitHub!
We built an interactive browser simulator where you can test L1/L2 cache promotion and trigger a 5,000-request stampede live:
- 🎮 Live Playground: https://zisam-glitch.github.io/kvdock/playground/index.html
- 📦 GitHub Repo: https://github.com/zisam-glitch/kvdock
If you find this useful or want to bookmark it for your next Next.js / TypeScript project, please drop a ★ Star on GitHub — every star helps us unlock our first 16-star milestone! Feedback and questions are super welcome in the comments below.
