Caching Strategies in 2026: Redis vs Memcached vs CDN
Arvucore Team
September 22, 2025 · Updated August 26, 2026
13 min read
Use cache-aside with a TTL plus explicit invalidation as the default, put a CDN in front of anything public and cacheable, and pick Redis over Memcached unless your only need is get/set of small values at very high concurrency. Everything else is about two problems: keeping data fresh enough, and making sure a cache miss does not take the database down.
The four cache layers and what each one is for
A request can be answered at four points, each with its own owner, TTL horizon, and invalidation mechanism.
| Layer | Where it lives | Typical TTL | Invalidation | Best for |
|---|---|---|---|---|
| Browser / HTTP | User's device | Minutes to a year | Versioned URLs, Cache-Control, ETag |
Static assets, fonts, immutable bundles |
| CDN / edge | Provider PoPs near the user | Seconds to days | Purge API, surrogate keys, s-maxage |
Public pages, images, cacheable API responses |
| Application | Redis, Memcached, or in-process memory | Seconds to hours | TTL, write-time invalidation, versioned keys | Rendered fragments, sessions, computed objects, rate limits |
| Database / query | DB buffer pool, materialized views, query cache | Managed by the DB | Automatic or scheduled refresh | Hot rows, expensive aggregates |
Design from the outside in: push whatever can be public to the CDN, keep per-user or fast-changing data in the application cache, and let the database cache handle what remains. In-process memory (a Map in Node, Caffeine on the JVM) is a fifth layer: fastest by far, but per instance and gone on deploy. Use it for configuration, feature flags, and very hot small objects in front of Redis.
Caching patterns: cache-aside, read-through, write-through, write-behind, refresh-ahead
The pattern decides who populates the cache and when writes reach it.
Cache-aside (lazy loading). The application checks the cache, reads the database on a miss, writes the value back, and returns it. It tolerates a cache outage, works with any store, and caches only what is read. The cost: every miss pays the full query, and every writer must remember to invalidate.
Read-through. Same read behavior, but the cache library or proxy loads from the database on a miss. Cleaner call sites, less flexibility.
Write-through. Writes go to the cache, which synchronously writes to the database. Never stale for data written through it, at the cost of write latency and of caching data nobody reads.
Write-behind (write-back). Writes go to the cache and are flushed asynchronously in batches. High write throughput, but if the cache node dies before the flush, the writes are gone. Only for data you can afford to lose (counters, view stats, presence).
Refresh-ahead. The cache refreshes a hot key in the background before it expires. Good for a few expensive, predictable keys; wasteful on a long tail.
| Pattern | Read miss cost | Write latency | Staleness | Data loss risk | Default for |
|---|---|---|---|---|---|
| Cache-aside | DB query + cache write | None | Until TTL or invalidation | None | Most read-heavy workloads |
| Read-through | Same, inside the cache layer | None | Until TTL or invalidation | None | Teams standardizing on one library |
| Write-through | Low (data usually present) | Cache + DB, synchronous | Minimal | None | Read-after-write consistency |
| Write-behind | Low | Cache only | Minimal for reads | Yes, until flushed | Counters, telemetry, high write rates |
| Refresh-ahead | Near zero for hot keys | None | Bounded by refresh interval | None | Few expensive, predictable keys |
If you are unsure, start with cache-aside behind a repository interface, as in hexagonal architecture; you can move individual keys to write-through or refresh-ahead later without touching callers.
Invalidation and TTL strategy
TTL and invalidation are not alternatives. A TTL bounds how stale data can get when invalidation fails; invalidation keeps data fresh when it works. Use both.
Choose TTLs per data class, not per service. A catalog that changes a few times a day can live for an hour; a stock level in checkout for a few seconds, if at all. Write the classes down; they are the contract the team implements against.
Invalidate on write, at the source. The code path that updates the database deletes the cache key or emits an event that does. Deleting is safer than updating: an update can race with a concurrent read that writes back an older value.
Version keys instead of purging a namespace. Put a version in the key (catalog:v42:product:123) and bump it on bulk updates. Old entries become unreachable and expire on their own.
Add jitter. Keys warmed together with the same TTL expire together. Randomize each TTL by 10 to 20 percent.
Use events for cross-service invalidation. In an event-driven architecture, the service that owns a table publishes ProductUpdated; consumers that cache product data subscribe and drop their keys. Redis pub/sub or keyspace notifications work for a single Redis deployment; a message broker is the right tool once several services are involved.
Stampede protection: locking, early expiry, request coalescing
The classic cache outage is not the cache going down. It is one popular key expiring under load: hundreds of requests miss at once, all run the same expensive query, and the database falls over. These techniques compose.
Per-key locking. On a miss, the first request takes a short lock (SET lock:key 1 NX PX 5000), rebuilds, writes, releases. Others wait briefly and re-read, or serve a stale copy. The lock TTL must exceed a normal rebuild.
Request coalescing (single-flight). Inside one process, deduplicate concurrent loads for the same key so only one database call is in flight. Go's singleflight is the reference; in Node it is a map of pending promises.
Probabilistic early expiry. Each read decides, with a probability that rises as expiry approaches, to refresh early. Hot keys get refreshed by one reader before they expire; cold keys are left alone. No scheduler needed.
Serve stale while rebuilding. Keep the logical TTL shorter than the physical TTL. In between, readers get the stale value immediately while one of them rebuilds.
Here is cache-aside with a lock and in-process coalescing in TypeScript, using the ioredis client:
import Redis from "ioredis";
const redis = new Redis();
const inflight = new Map<string, Promise<string>>();
export async function cached(
key: string,
ttlSec: number,
load: () => Promise<string>,
): Promise<string> {
const hit = await redis.get(key);
if (hit !== null) return hit;
// Coalesce concurrent misses inside this process.
const pending = inflight.get(key);
if (pending) return pending;
const task = (async () => {
const lockKey = `lock:${key}`;
const gotLock = await redis.set(lockKey, "1", "PX", 5000, "NX");
if (!gotLock) {
// Another instance is rebuilding; wait briefly and re-check.
await new Promise((r) => setTimeout(r, 50));
const again = await redis.get(key);
if (again !== null) return again;
// Fall through and load anyway rather than block forever.
}
try {
const value = await load();
const jitter = Math.floor(ttlSec * (0.9 + Math.random() * 0.2));
await redis.set(key, value, "EX", jitter);
return value;
} finally {
if (gotLock) await redis.del(lockKey);
}
})();
inflight.set(key, task);
try {
return await task;
} finally {
inflight.delete(key);
}
}
The fallthrough after a failed lock is deliberate: blocking every waiter turns a slow rebuild into an outage, while letting a few load in parallel bounds the damage.
Consistency trade-offs you are actually making
A cache is a replica with a weaker consistency model than the database. Be explicit about which model each data class gets.
- Eventual with a bound. Cache-aside plus TTL. Readers may see data up to TTL seconds old. Fine for catalogs, content, search results, and most dashboards.
- Read-your-writes. After a user updates something, they must see it. Delete the key on write and bypass the cache for that user's next read, or write-through for that key. Sessions and profile edits need this.
- Monotonic reads. A user must never see data go backwards. Breaks when two instances hold different cached versions. Fix with versioned keys or by routing a user consistently to one replica.
- Strong. Do not cache. Inventory decrements, balances, and payment state go to the database.
The most common production bug is a mismatch: eventual consistency applied to a field the product treats as strong. Classify first; choose the pattern second.
Redis vs Memcached: comparison table
| Criterion | Redis | Memcached |
|---|---|---|
| Data structures | Strings, hashes, lists, sets, sorted sets, streams, bitmaps, HyperLogLog, geospatial, JSON and search modules | Strings only (opaque byte values) |
| Persistence | Optional: RDB snapshots, AOF log, or both | None; restart means empty cache |
| Replication and HA | Primary-replica, Sentinel for failover, Redis Cluster for sharding with failover | None built in; clients shard with consistent hashing |
| Clustering | Native (Redis Cluster, hash slots, resharding) | Client-side or proxy-side only |
| Memory efficiency | Higher per-key overhead; small hashes and sets are encoded compactly | Slab allocator, very low overhead per key; can waste memory across slab classes |
| Threading | Single-threaded command execution with I/O threads; scale by sharding | Fully multithreaded; one large node scales with cores |
| Atomic operations | Rich: INCR, SETNX, MULTI/EXEC, Lua scripts, Functions | INCR/DECR, CAS, add/replace |
| Eviction | Configurable: LRU, LFU, TTL-based, volatile or all keys | LRU per slab class |
| Pub/sub and streams | Yes | No |
| Max value size | 512 MB | 1 MB default (configurable) |
| Operational surface | Larger: persistence tuning, replication lag, cluster topology | Minimal: memory and connections |
| Licensing note | Check the license of the specific distribution; Redis, Valkey (Linux Foundation fork), and cloud-managed variants differ | Open source, BSD |
| Typical use cases | Sessions, rate limiting, leaderboards, queues, locks, feature flags, computed objects, real-time counters | Pure object cache for rendered fragments, ORM results, API responses |
Choose Memcached for a large, flat, multithreaded object cache with simple values under 1 MB, where losing everything on restart is acceptable. Its simplicity is the feature.
Choose Redis when you need any data structure, atomic operations, locks, pub/sub, persistence, or built-in replication. That describes most application caches once they grow past get/set, which is why Redis (or Valkey, its community fork) is the default today. If you already run Redis for sessions or queues, a second system just for caching rarely pays off. Every major cloud offers both managed; check version lag, failover behavior, and per-node pricing before committing to features like Cluster.
CDN caching: Cache-Control, stale-while-revalidate, and surrogate keys
The CDN is the cheapest layer per request and the easiest to get wrong, because the rules live in HTTP headers most application code never sets deliberately.
Cache-Control is the contract. Separate browser and edge lifetimes:
Cache-Control: public, max-age=60, s-maxage=3600, stale-while-revalidate=300, stale-if-error=86400
max-age governs the browser, s-maxage governs shared caches (the CDN), stale-while-revalidate lets the edge serve an expired copy while it refetches in the background, and stale-if-error lets it keep serving if the origin returns 5xx. For per-user responses set private or no-store; a Set-Cookie header on a cacheable response is a classic way to leak one user's page to another.
Immutable assets get long lifetimes. Fingerprint file names at build time (app.3f9a1c.js) and send Cache-Control: public, max-age=31536000, immutable. Invalidate by changing the URL, never by purging; every modern bundler does this by default.
Surrogate keys make purge precise. Tag each response with the entities it depends on (Surrogate-Key: product-123 category-9, or the provider's Cache-Tag). When product 123 changes, purge that tag and every page that rendered it leaves the edge in one call. Wire this to the same write-time event that clears Redis: one event, two purges. Prefer a soft purge (mark stale, serve stale, refetch) so a bulk update does not send a wall of misses to the origin.
Origin shielding. Enable the provider's shield or tiered cache so all PoPs fetch from one regional cache; most CDNs also coalesce concurrent misses for the same URL.
For static and content-heavy sites, a Jamstack build pushes the cache decision all the way to deploy time, and the CDN becomes the origin.
Observability: hit ratio, p99, and the metrics that predict outages
A cache without metrics is a guess. Instrument these from day one.
- Hit ratio per key class, not just global. A high global ratio can hide a poor one on the single query that matters.
- Origin p99 latency and database load alongside hit ratio. If p99 spikes every time hit ratio dips a few points, you have a stampede risk.
- Evictions per second and memory fragmentation. Rising evictions with stable traffic means the cache is too small or TTLs too long (
INFO memoryandINFO statsin Redis;stats slabsin Memcached). - Rebuild time for the most expensive keys. This sets the lock TTL and the stale window.
- Replication lag, if reads go to Redis replicas.
- CDN hit ratio by path and status, plus purge latency.
Alert on trends rather than thresholds: a hit-ratio drop over ten minutes, or p99 above SLO for a sustained window. Feed it into the same error handling and logging pipeline as the rest of the platform.
Decision checklist
Work through this before adding or changing a cache.
- What is the cost of a miss? If it is a cheap indexed query, you may not need the cache yet. Measure first.
- What consistency does this data require? Eventual, read-your-writes, monotonic, or strong. Strong means no cache.
- Which layer answers cheapest? Public and identical for everyone: CDN. Per-user or fast-changing: application cache. Immutable asset: browser with a fingerprinted URL.
- Which pattern? Default cache-aside. Write-through for read-after-write. Write-behind only for loss-tolerant data. Refresh-ahead for a few expensive hot keys.
- What is the TTL, and what invalidates before it? Write both down. Add jitter.
- What happens when the key expires under load? Lock, coalesce, serve stale, or early-expire; at least one for every key whose rebuild is slower than a simple query.
- Redis or Memcached? Anything beyond get/set, or persistence or HA: Redis (or Valkey). Flat object cache, maximum simplicity: Memcached.
- How will you know it works? Hit ratio per class, origin p99, evictions, rebuild time, purge latency, all before rollout.
- How does the cache fail? Cache-aside must degrade to the database behind a circuit breaker so a Redis outage is slow, not down. Test it.
- Who owns invalidation when a second service writes the same table? If the answer is "nobody", use events.
Recommendation
Start with cache-aside on Redis (or Valkey), TTLs per data class with jitter, delete-on-write invalidation, and a per-key lock with in-process coalescing for any key whose rebuild is expensive. Put a CDN in front of every public response with explicit Cache-Control, stale-while-revalidate, and surrogate keys wired to the same invalidation events. Reserve Memcached for the flat, multithreaded object cache where its simplicity is the point. Instrument hit ratio per key class against origin p99 before you ship; that pair tells you whether the cache is a convenience or a load-bearing wall. At Arvucore we usually recommend classifying data by consistency requirement first and choosing stores and patterns second; teams that do it the other way around end up debugging staleness in production.
Ready to Transform Your Business?
Let's discuss how our solutions can help you achieve your goals. Get in touch with our experts today.
Talk to an ExpertTags:
Arvucore Team
Arvucore’s editorial team is formed by experienced professionals in software development. We are dedicated to producing and maintaining high-quality content that reflects industry best practices and reliable insights.
Frequently asked questions
- What is the difference between Redis and Memcached?
- Memcached is a simple, multithreaded key-value cache for strings. Redis adds data structures, persistence, replication, clustering, Lua scripting, and pub/sub. Pick Memcached for plain object caching at very high concurrency; pick Redis when you need anything beyond get/set.
- Which caching pattern should I use by default?
- Cache-aside. The application reads the cache, falls back to the database on a miss, and writes the result back with a TTL. It is simple, tolerates cache outages, and works with any store.
- What is a cache stampede and how do I prevent it?
- A stampede happens when a popular key expires and many requests hit the database at the same time to rebuild it. Prevent it with a per-key lock, request coalescing inside the process, or probabilistic early expiry that refreshes the key before it actually expires.
- How long should a cache TTL be?
- As long as the business can tolerate stale data, and no longer. Combine a TTL with explicit invalidation on writes so the TTL is only a safety net, not the primary freshness mechanism.
- What does stale-while-revalidate do in CDN caching?
- It lets the CDN serve an expired response immediately while it fetches a fresh copy from the origin in the background. Users never wait for the origin, and the origin only sees one revalidation per edge instead of a burst.
- What cache hit ratio is good?
- It depends on the cost of a miss, not on a universal number. Track hit ratio together with origin p99 latency and database load; if a small drop in hit ratio makes p99 jump, the cache is doing critical work and needs stampede protection.
Related articles

GraphQL vs REST in 2026: When to Use Each API Style
GraphQL vs REST compared on fetching, caching, versioning, errors, N+1, security and tooling, with a decision checklist by scenario and a side-by-side example.

Microservices Testing Strategies for Distributed Systems
Microservices testing is essential for ensuring resilience, scalability and reliability in modern architectures. This article from Arvucore outlines practical testing strategies for distributed systems testing, helping technical teams and decision makers design robust pipelines, select tools, and measure quality. It balances business concerns with engineering realities, referencing established best practices and market insights for pragmatic implementation.

Microservices vs. Monolithic Architecture: Which to Choose for Your Company
At Arvucore we help European business and technical leaders decide between microservices vs monolithic approaches. This article examines enterprise software architecture trade-offs, focusing on application scalability, operational cost, and time-to-market. Expect practical decision criteria, migration patterns, and risk mitigation techniques grounded in industry reports and best practices to help you choose the right architecture for your company.

API Gateway: Enterprise API Management and Security
An API gateway is central to enterprise API management and security, providing routing, policy enforcement, and observability for distributed systems. This article from Arvucore explores practical api gateway implementation approaches, governance and lifecycle strategies, and robust microservices security patterns. It helps business and technical leaders evaluate trade-offs, reduce integration risk, and accelerate secure API adoption across complex IT landscapes.