What Is Distributed Caching?
Distributed caching stores cached data across multiple networked cache nodes so many application instances can share fast access to frequently used data. Instead of keeping cached values only inside one application process, the cache becomes a separate distributed layer.
Distributed caches such as Redis-based clusters are commonly used to reduce database load, improve response latency, share state across application instances, and scale caching beyond the memory available on a single machine. The trade-off is additional network, consistency, partitioning, failure-handling, and operational complexity.
Table of Contents
- Why Distributed Caching?
- How Distributed Caching Works
- Local Cache vs Distributed Cache
- How Data Is Distributed Across Cache Nodes
- Replication and High Availability
- Cache Consistency
- Expiration and Eviction
- Common Distributed Caching Patterns
- Distributed Cache Failure Scenarios
- Production Design Example
- Common Distributed Caching Mistakes
- Production Checklist
- Frequently Asked Questions
- Conclusion
Why Distributed Caching?
Consider an application running on one server. Frequently requested data can be stored directly in that process's memory:
Client
|
v
Application
|
+-- Local Cache
|
v
Database
This is extremely fast because cache access does not require a network call.
The architecture becomes more complicated when the application scales horizontally:
Load Balancer
/ | \
v v v
App 1 App 2 App 3
| | |
Cache Cache Cache
\ | /
Database
Each application instance has its own independent cache. A value cached by App 1 is unavailable to App 2. The same data may be loaded repeatedly, and invalidating copies across instances becomes more difficult.
A distributed cache creates a shared caching layer:
Load Balancer
/ | \
v v v
App 1 App 2 App 3
\ | /
\ | /
Distributed
Cache
|
v
Database
Every application instance can now access the same cached values.
This provides several benefits:
- shared cached data across application instances;
- reduced database traffic;
- lower response latency;
- larger aggregate cache capacity;
- independent scaling of application and cache layers;
- centralized expiration and invalidation behavior.
The broader role of caching in scalable applications is covered in Caching Explained: Improving Performance Without Overloading Databases.
How Distributed Caching Works
A distributed cache usually sits between application servers and slower persistent storage.
Application
|
v
Distributed Cache
|
+--+--+
| |
HIT MISS
| |
v v
Return Database
|
v
Cache value
|
v
Return
For a read operation, the application first attempts to retrieve a value from the cache.
async def get_product(product_id: str):
key = f"product:{product_id}"
cached = await cache.get(key)
if cached is not None:
return cached
product = await database.get_product(product_id)
if product is not None:
await cache.set(
key,
product,
ttl=300,
)
return product
A cache hit avoids the database entirely.
A cache miss loads the value from persistent storage and usually stores it in the cache for future requests.
If the endpoint receives 20,000 requests per second but 95% are cache hits, only a fraction of those requests need to reach the database.
20,000 requests/sec
|
v
Distributed Cache
/ \
/ \
19,000 hits 1,000 misses
|
v
Database
The exact numbers depend on workload shape, key popularity, TTLs, invalidation, and cache capacity, but the architectural goal is to prevent repeated expensive work.
Local Cache vs Distributed Cache
Local and distributed caches solve related but different problems.
| Characteristic | Local Cache | Distributed Cache |
|---|---|---|
| Location | Application process | Separate networked service |
| Access latency | Very low | Includes network latency |
| Shared across instances | No | Yes |
| Total capacity | Limited by application memory | Can span multiple nodes |
| Consistency across instances | More difficult | Centralized |
| Failure dependency | Application instance | Cache infrastructure |
A distributed cache is slower than an in-process dictionary because every lookup requires network communication. It is still normally much faster than performing an expensive database query or calling another service.
The two approaches can also be combined:
Application
|
v
L1 Local Cache
|
MISS
|
v
L2 Distributed Cache
|
MISS
|
v
Database
This is a multi-level cache. Extremely popular values can be served locally while the distributed layer provides shared caching across application instances.
Designing Multi-Level Caching Architectures covers this design separately.
How Data Is Distributed Across Cache Nodes
A single cache server has finite memory, CPU, and network capacity. Large systems therefore distribute keys across multiple nodes.
Cache Cluster
/ | \
v v v
Node A Node B Node C
product:1 ────────> Node A
product:2 ─────────────────> Node C
product:3 ─────────────> Node B
A hash of the key can determine which node stores the value:
hash(cache_key)
|
v
cache node
A simplified approach might look like:
def node_for_key(key: str, nodes: list[str]) -> str:
index = hash(key) % len(nodes)
return nodes[index]
Simple modulo hashing has a significant weakness: adding or removing a node can remap a large percentage of keys.
Distributed cache systems can instead use partitioning schemes designed to reduce unnecessary movement when topology changes. What Is Consistent Hashing? explains one common approach.
Partitioning increases total capacity, but it does not guarantee perfectly balanced traffic. A single extremely popular key can still overload the node responsible for it.
Replication and High Availability
Partitioning determines where data belongs. Replication provides additional copies of data for availability and, in some designs, read scalability.
Cache Shard
/ \
v v
Primary Replica
|
writes
reads may use
one or more copies
If the primary node fails, a replica may be promoted:
Primary
X
|
v
Replica promoted
|
v
New Primary
Replication reduces the impact of individual node failures, but it does not make failures invisible.
Applications should still expect:
- brief connection failures;
- timeouts during failover;
- temporary loss of some cache entries;
- replication lag;
- retries to another node;
- increased load after topology changes.
For ordinary cached data, losing some entries is often acceptable because they can be rebuilt from the source of truth.
If the cache contains state that cannot safely be reconstructed, its durability requirements are fundamentally different and need explicit design.
Cache Consistency
A cache contains a copy of data that normally exists somewhere else. Once the original data changes, the cached copy can become stale.
Database
price = $59
Cache
price = $59
|
Database update
v
Database
price = $49
Cache
price = $59 ← stale
The system needs a strategy for keeping cache state sufficiently current.
Common options include:
- TTL-based expiration;
- explicit invalidation after database changes;
- updating the cache when data changes;
- event-driven invalidation;
- versioned cache keys.
Perfect synchronization is often unnecessary. The correct strategy depends on how stale the data is allowed to become.
For example, a product description might tolerate seconds of staleness, while permissions or inventory decisions may require much stronger freshness.
Cache invalidation deserves explicit design rather than being treated as an implementation detail. Cache Invalidation Strategies for Production Systems covers the main approaches and trade-offs.
Expiration and Eviction
Distributed caches have finite capacity. Cached values therefore cannot remain indefinitely unless the working set fits comfortably within available memory.
Expiration removes a key according to a configured lifetime:
SET product:481
TTL = 300 seconds
|
v
5 minutes later
key expires
Eviction removes cached data because the cache needs memory for other values.
Cache memory full
|
v
Eviction policy
|
v
Remove selected keys
TTL and eviction solve different problems. TTL primarily controls freshness and lifetime. Eviction controls finite memory usage.
An application should be designed so an evicted value behaves like a normal cache miss:
Cache MISS
|
v
Load source
|
v
Repopulate cache
If losing a key causes incorrect business behavior rather than merely additional work or latency, that key may represent application state rather than disposable cached data.
Common Distributed Caching Patterns
The relationship between the application, cache, and database depends on the caching strategy.
Cache-aside lets the application explicitly load missing values:
Application → Cache
MISS
Application → Database
Application → Cache
This is one of the most common distributed caching patterns. What Is Cache-Aside? explains it in detail.
Write-through updates the cache and persistent storage as part of the write path:
Application
|
v
Cache
|
v
Database
Write-behind persists changes asynchronously:
Application
|
v
Cache
|
+----> immediate completion
|
v
Asynchronous persistence
|
v
Database
Each pattern makes different trade-offs around latency, freshness, write amplification, failure behavior, and implementation complexity.
Distributed Cache Failure Scenarios
A distributed cache improves performance, but it is also another distributed dependency. Production architecture should define behavior when parts of the caching layer fail.
Cache Node Failure
If one cache node disappears, requests for its keys may temporarily become misses.
Node A ✓
Node B X
Node C ✓
Keys from Node B
|
v
cache misses
|
v
database
This can create a sudden increase in database traffic. Replication can reduce the impact, but applications should still protect the backend during failover and cache rebuilding.
Complete Cache Outage
A dangerous fallback is to send every request directly to the database:
100,000 req/s
|
X Cache
|
v
100,000 req/s
Database
The cache outage can immediately become a database outage.
Safer designs can use concurrency limits, request throttling, load shedding, stale local data, circuit breakers, or degraded responses depending on application requirements.
A cache should improve capacity without making the underlying database incapable of surviving realistic cache failures.
Cache Stampede
When a popular cached value expires, many requests may discover the miss simultaneously and all attempt to regenerate the same value.
Popular key expires
|
v
many cache misses
/ | | \
v v v v
DB DB DB DB
This is a cache stampede. Request coalescing, refresh locks, stale-while-revalidate, proactive refresh, and TTL jitter can reduce the risk.
The dedicated What Is a Cache Stampede? article covers the problem in detail.
Hot Keys
Traffic is not always evenly distributed across cached keys.
Node A Node B Node C
15% 97% 18%
^
|
hot key
One extremely popular key can overload the node responsible for it even when the overall cluster has substantial unused capacity.
This is known as a hot key. Local caching, replication, request distribution, and workload-specific sharding can help depending on whether the key is read-heavy or write-heavy.
What Is a Hot Key? explains this failure mode separately.
Production Design Example
Consider an e-commerce API running across 20 application instances. Product pages are read frequently, while product data changes relatively infrequently.
Clients
|
v
Load Balancer
|
+-----------+-----------+
| | |
v v v
API-1 API-2 API-N
\ | /
\ | /
+---------+---------+
|
v
Redis Cluster
/ | \
v v v
Shard A Shard B Shard C
|
v
PostgreSQL
The application uses cache-aside for product reads.
import json
PRODUCT_TTL = 300
async def get_product(product_id: str):
key = f"product:{product_id}"
cached = await redis.get(key)
if cached is not None:
return json.loads(cached)
product = await database.get_product(product_id)
if product is None:
return None
await redis.set(
key,
json.dumps(product),
ex=PRODUCT_TTL,
)
return product
Product updates invalidate the corresponding cached value after the database transaction succeeds:
async def update_product(product_id: str, changes):
product = await database.update_product(
product_id,
changes,
)
await redis.delete(
f"product:{product_id}"
)
return product
The next read repopulates the cache with the new database state.
The cache cluster partitions keys across nodes:
product:1001 → Shard A
product:1002 → Shard C
product:1003 → Shard B
product:1004 → Shard A
Each shard has a replica so a single cache-node failure does not require rebuilding every key assigned to that shard.
The design also protects the database from cache failure.
If Redis becomes unavailable, the API does not allow unlimited concurrent database fallback queries. A semaphore or similar concurrency control can bound expensive database work:
import asyncio
db_fallback_limit = asyncio.Semaphore(200)
async def load_with_bounded_fallback(product_id: str):
async with db_fallback_limit:
return await database.get_product(product_id)
This may increase request latency or cause some requests to fail under extreme load, but it protects the database from receiving an uncontrolled traffic surge.
For extremely popular products, the architecture can add a short-lived local L1 cache:
API Instance
|
v
Local L1 Cache
|
MISS
|
v
Redis L2 Cache
|
MISS
|
v
PostgreSQL
This reduces network traffic toward Redis for hot values while Redis continues to provide a shared cache across application instances.
Useful production metrics include:
| Metric | Why It Matters |
|---|---|
| Cache hit rate | Shows how effectively the cache avoids backend work |
| Cache miss rate | Detects expiration, eviction, or workload changes |
| Cache latency | Shows whether cache access remains beneficial |
| Memory utilization | Detects capacity pressure |
| Eviction rate | Shows whether useful values are being removed |
| Traffic per node | Detects uneven distribution and hot keys |
| Connection count | Detects client or server connection pressure |
| Database fallback rate | Shows backend impact from misses or cache failures |
Cache hit rate should not be evaluated alone. A 99% hit rate can still be problematic if the remaining 1% represents expensive queries or a concentrated burst against one backend resource.
Common Distributed Caching Mistakes
- Caching everything. Values with low reuse may consume memory without reducing meaningful backend work.
- Using the cache as the source of truth accidentally. Disposable cached data and durable application state have different reliability requirements.
- Ignoring invalidation. Fast stale data can be worse than slower correct data.
- Using one TTL for every key. Different data has different freshness and access characteristics.
- Giving many keys identical expiration times. Synchronized expiration can create backend traffic spikes.
- Assuming hashing guarantees balanced load. Keys can be evenly distributed while traffic remains highly skewed.
- Ignoring hot keys. One popular key can overload a single cache node.
- Allowing unlimited database fallback. A cache outage can cascade into a database outage.
- Retrying cache requests aggressively. Retries can amplify load during a cache incident.
- Ignoring network latency. Distributed cache access is not equivalent to local-memory access.
- Creating too many connections. Large application fleets can overwhelm the cache with connection churn.
- Ignoring serialization cost. Large cached objects can consume significant CPU, memory, and network bandwidth.
- Monitoring only hit rate. Latency, evictions, node skew, failures, and backend fallback matter as well.
Production Checklist
- Cache data with meaningful reuse.
- Define the source of truth for every cached value.
- Choose a caching pattern explicitly.
- Define freshness requirements.
- Choose TTLs based on data semantics.
- Add TTL jitter where synchronized expiration is possible.
- Define cache invalidation behavior.
- Choose an appropriate partitioning strategy.
- Plan for node additions and removals.
- Use replication where availability requirements justify it.
- Understand failover behavior.
- Detect hot keys and uneven node traffic.
- Protect against cache stampedes.
- Bound database fallback concurrency.
- Define behavior during a complete cache outage.
- Use timeouts for cache operations.
- Use bounded retries with backoff.
- Control cache connection counts.
- Avoid unnecessarily large cached objects.
- Monitor cache hit and miss rates.
- Monitor cache latency.
- Monitor memory utilization.
- Monitor eviction rate.
- Monitor traffic per node and shard.
- Monitor backend fallback traffic.
- Test individual cache-node failures.
- Test complete cache outages.
- Test popular-key expiration under concurrency.
- Test application behavior during cache failover.
Frequently Asked Questions
Distributed caching is primarily a performance and scalability technique, but introducing a shared cache also introduces distributed-system concerns such as network failures, partitioning, replication, consistency, and traffic skew.
Is a Distributed Cache a Database?
A distributed cache can provide database-like capabilities, but its role is usually different. Cached data is typically derived from another source and can be regenerated if necessary.
If losing a value means permanent business-data loss, that value should not be treated as disposable cache data without additional durability guarantees.
Does Distributed Caching Require Redis?
No. Redis is a common technology for distributed caching, but distributed caching is an architectural concept rather than a specific product.
Different systems can provide shared in-memory key-value storage, partitioning, replication, expiration, and eviction capabilities.
Should the Cache Be the Source of Truth?
Usually not for conventional caching. A cache normally stores data that can be reconstructed from a database, service, or another authoritative source.
Architectures such as write-behind can temporarily place newer state in the caching layer, but this changes the durability model and requires explicit handling of pending persistence.
Can Local and Distributed Caching Be Used Together?
Yes. A local L1 cache can serve extremely frequent requests without network access, while a distributed L2 cache shares data across application instances.
The trade-off is additional invalidation and consistency complexity because the same logical value can exist in multiple cache layers.
When Is a Distributed Cache Worth Using?
A distributed cache is useful when multiple application instances repeatedly access expensive-to-load data and a shared caching layer can materially reduce database, API, or computation load.
For small applications with inexpensive database queries and modest traffic, introducing a distributed cache can add more operational complexity than value. Caching should solve a measured performance or scalability problem rather than be added automatically.
Conclusion
Distributed caching provides a shared high-speed data layer that can reduce database load and improve application latency across horizontally scaled application instances. Partitioning allows cache capacity to grow across multiple nodes, while replication can improve availability.
The performance benefits come with distributed-system trade-offs. Cached data can become stale, nodes can fail, keys can expire simultaneously, traffic can concentrate on hot keys, and a cache outage can suddenly expose the backend to much more traffic. Production designs therefore need explicit strategies for invalidation, expiration, partitioning, replication, stampede prevention, failure handling, and observability.
Key Takeaway: distributed caching is not simply a faster database layer. It is shared, failure-prone infrastructure that trades additional architectural complexity for lower latency and reduced backend load. Design both the cache-hit path and the failure path before depending on it for production scale.
Comments (0)