Caching is the cheapest performance win available to most API services — it reduces latency, cuts database queries, and absorbs traffic spikes that would otherwise collapse your origin. The mistake most teams make is either skipping it entirely or caching everything with the same TTL and then fighting stale data. The 2026 approach is layered and intentional.
What changed in 2026
stale-while-revalidate became widely understood and is now the default strategy for edge-cached API responses — serve the cached version immediately, refresh in the background.
- Valkey (the Redis fork) stabilised as the open-source default after Redis relicensed; most managed services (Upstash, Elasticache) now offer both.
- CDN providers added API-aware caching. Cloudflare Cache API and Fastly's Compute allow per-route cache logic at the edge without hitting your origin.
- ORM-level caching emerged. Drizzle and Prisma both offer query-result caching integrations, making the application layer easier to cache without explicit Redis calls.
The caching layers
| Layer |
Where it lives |
Best for |
| In-process (Map / LRU) |
App memory |
Single-instance, read-heavy, small datasets |
| Shared cache (Redis/Valkey) |
Separate service |
Multi-instance, session data, computed results |
| HTTP cache (CDN/proxy) |
Edge / browser |
Public API responses, static data |
| Database query cache |
DB or ORM layer |
Expensive query results |
Use the lowest layer that meets your needs. In-process is fastest; HTTP cache scales for free.
HTTP cache headers (free wins)
# Public response, cache for 60 s, serve stale up to 10 s while refreshing
Cache-Control: public, max-age=60, stale-while-revalidate=10
# Private (per-user) response, no proxy caching
Cache-Control: private, max-age=300
# Never cache (mutations, auth tokens)
Cache-Control: no-store
Set ETag or Last-Modified on responses so clients can make conditional requests that return 304 Not Modified when the content has not changed — reducing body transfer costs.
Redis caching patterns
Cache-aside (lazy loading) is the most common pattern:
async function getProduct(id: string): Promise<Product> {
const cached = await redis.get(`product:${id}`);
if (cached) return JSON.parse(cached);
const product = await db.products.findUnique({ where: { id } });
if (!product) throw new NotFoundError();
await redis.setex(`product:${id}`, 300, JSON.stringify(product)); // TTL 5 min
return product;
}
Write-through keeps cache and DB in sync on every write — higher write cost, but no cold cache on read:
async function updateProduct(id: string, data: Partial<Product>) {
const updated = await db.products.update({ where: { id }, data });
await redis.setex(`product:${id}`, 300, JSON.stringify(updated));
return updated;
}
Cache invalidation
TTL-based invalidation is simple but causes stale windows. For tighter consistency, use event-driven invalidation:
// On product update event:
await redis.del(`product:${productId}`);
await redis.del(`product-list:category:${product.categoryId}`);
For complex relationships, use versioned keys: product:${id}:v${version}. Increment the version on write; old keys expire naturally.
How to pick a caching strategy
- Public, user-agnostic data? HTTP
Cache-Control + CDN. Zero extra infrastructure.
- Computed results shared across users? Redis cache-aside with a TTL matching your staleness tolerance.
- Per-user personalised data?
Cache-Control: private at HTTP; Redis with a user-scoped key.
- Expensive aggregations? Pre-compute on a schedule and cache the result — background job writes, API reads.
- Real-time data that must never be stale? Do not cache; use a WebSocket or server-sent events.
Common mistakes
Caching with one TTL for everything. Product listings and user profile pictures have wildly different staleness tolerance. Set TTLs per resource type.
Forgetting cache on write paths. Cache-aside only fills on reads; a surge of reads after a cold deploy hammers the DB. Pre-warm critical keys on startup.
Not caching errors. A flapping downstream service will miss-cache on every request. Cache short-TTL error states (5–15 s) to absorb the load.
Using request bodies as cache keys. POST responses almost never should be cached, and POST bodies are not reliably comparable as keys.
No cache hit rate monitoring. A cache with a 30% hit rate might as well not exist. Track hit rate and miss latency; alert when hit rate drops.
What to skip
- Caching mutations (POST, PUT, DELETE) — never. Return fresh data or redirect.
- In-process caches for multi-instance services without a shared invalidation mechanism — different pods serve different data.
- Infinite TTLs without an invalidation path — your cache will drift from reality with no way back.
FAQ
What TTL should I use?
Match it to how often the underlying data actually changes. Product prices: minutes. Static reference data: hours or days. User-generated content: seconds to minutes.
How do I avoid cache stampedes?
Use a probabilistic early expiration (PER) or a mutex lock on cache miss so only one request rebuilds the cache while others wait briefly.
Is Redis still the right choice in 2026?
Yes for most teams. Valkey (the open-source Redis fork) is a drop-in replacement. Upstash offers serverless Redis with per-request billing that works well for lower-traffic APIs.
How do I cache in a serverless function?
In-process caching does not survive cold starts. Use an external store (Redis/Upstash) or rely on HTTP cache headers with a CDN sitting in front.
Where to go next
See How to paginate an API in 2026, How to monitor a service in 2026, and How to optimize SQL queries in 2026.