Redis can make a backend dramatically faster.
It can also make a backend dramatically more fragile if the architecture treats the cache as a dependency that must always be available.
The distinction is simple:
A database stores authoritative data. A cache stores data that can be recreated.
Once that distinction is understood, Redis failure becomes an architectural problem rather than simply an infrastructure outage.
The Cache-Aside Pattern
Consider a Node.js backend serving product information.
Without caching:
Client
│
▼
Node.js API
│
▼
Database
│
▼
Response
With Redis:
Client
│
▼
Node.js API
│
▼
Redis
│
├── Cache Hit ─────► Response
│
└── Cache Miss
│
▼
Database
│
▼
Redis SET
│
▼
Response
This reduces repeated database reads and can significantly improve response latency for frequently accessed data.
But caching introduces a new dependency and therefore a new failure mode.
What Happens When Redis Goes Down?
Suppose the API expects Redis to be available for every request.
A naive implementation might do this:
Request
│
▼
Redis
│
X
Redis unavailable
│
▼
Request fails
That is often unnecessary.
If Redis only contains cached data, the application may safely fall back to the primary datastore:
Request
│
▼
Redis
│
X
unavailable
│
▼
Database
│
▼
Response
This is a much better failure model.
But it is not the end of the problem.
The Dangerous Fallback
Imagine Redis normally handles 90% of read traffic.
Now Redis fails.
Suddenly:
Normal
1000 requests ─────────────►
Redis
↓
Database
100
After Redis fails:
Redis DOWN
1000 requests ─────────────► Database
│
▼
Database overloaded
│
▼
API latency ↑
│
▼
Timeouts ↑
│
▼
More retries
│
▼
More overload
The fallback has created a failure cascade.
This is one of the most important lessons in distributed systems:
A fallback can move the failure rather than eliminate it.
Timeouts Are a Reliability Feature
A common mistake is allowing an API request to wait indefinitely for Redis.
If Redis is slow, every request waiting on Redis consumes application resources.
Instead, external dependencies should have deliberate timeouts.
Conceptually:
API Request
│
▼
Redis request
│
├── Fast response ──► Continue
│
└── Timeout ────────► Fallback
The timeout should be based on the application's latency requirements rather than an arbitrary number.
A cache that normally responds in milliseconds should not be allowed to hold an API request for several seconds before the application decides it is unavailable.
Circuit Breakers Prevent Repeated Failure
If Redis is continuously failing, repeatedly attempting Redis requests is wasteful.
A circuit breaker changes the behavior:
Redis healthy
│
▼
CLOSED
│
repeated failures
│
▼
OPEN
│
skip Redis calls
│
▼
Database
After a controlled recovery period, the circuit can allow limited requests to test whether Redis has recovered.
This prevents every request from repeatedly hitting a known unhealthy dependency.
Retries Can Make an Outage Worse
Retries are useful for transient failures.
But retries are dangerous when they are uncontrolled.
Suppose 1,000 requests fail because Redis is unavailable.
If every request retries three times:
1,000 original requests
×
3 retries
=
3,000 additional attempts
The dependency is already unhealthy, and the application is now generating more traffic toward it.
This is why retries should generally be paired with:
timeouts,
exponential backoff,
retry limits,
jitter,
circuit breakers.
The goal is not to retry forever.
The goal is to distinguish transient failure from persistent failure.
Cache Stampede: Another Hidden Failure
There is another scenario worth considering.
Suppose a popular cached value expires at the same time for thousands of users.
Instead of:
Redis ──► cached response
thousands of requests simultaneously execute:
Redis MISS
↓
Database
↓
Database
↓
Database
↓
Database
↓
...
This is commonly called a cache stampede.
Possible mitigation strategies include:
TTL randomization,
request coalescing,
stale-while-revalidate,
background refresh,
controlled cache warming.
The important lesson is that cache expiration is itself a traffic event.
Stale Data Can Be Better Than No Data
Not every piece of data requires perfect freshness.
For example, a product catalog, configuration metadata, or public article may tolerate slightly stale information.
In those cases, serving a stale value can be better than failing the request.
Conceptually:
Fresh cache
│
▼
Return immediately
Fresh cache unavailable
│
▼
Stale value available?
│
├── Yes ──► Return stale value
│
└── No ───► Query database
This is an example of graceful degradation.
The system preserves useful functionality even when one component is unhealthy.
Redis Should Have a Clear Responsibility
A clean architecture establishes boundaries.
Database:
Source of truth
Redis:
Performance optimization
Node.js:
Application and business logic
Load balancer:
Traffic distribution
Monitoring:
Detection and diagnosis
Once responsibilities are clear, failure behavior becomes easier to reason about.
If Database fails, the application has a fundamentally different problem than if Redis fails.
If Redis fails, the system may degrade in performance.
If the primary database fails, the system may lose its source of truth.
Treating both failures identically is an architectural mistake.
Monitor the Cache, Not Just the Application
A backend can report HTTP 200 responses while quietly becoming unhealthy.
Useful cache metrics include:
cache hit ratio,
cache miss ratio,
Redis latency,
connection errors,
timeout rate,
memory usage,
eviction rate,
command latency,
connection count.
A falling cache hit rate may indicate that the cache is undersized, poorly keyed, expiring too aggressively, or simply not providing enough value for the workload.
The goal of observability is not to collect every metric possible. It is to detect behavior that requires action. AWS similarly recommends monitoring all workload components and reviewing whether monitoring coverage and thresholds remain appropriate.
Test Redis Failure Before Production Finds It
A resilient design should be tested intentionally.
For example:
Test 1:
Stop Redis
↓
Does the API remain available?
Test 2:
Add Redis latency
↓
Do API timeouts remain bounded?
Test 3:
Generate high read traffic
↓
Does Database survive cache failure?
Test 4:
Expire a popular key
↓
Does a cache stampede occur?
These tests reveal something a normal happy-path test cannot:
how the system behaves when dependencies stop behaving normally.
Failure testing is a core part of resilience engineering because recovery behavior should be understood before a real incident forces the lesson.
The Real Goal Is Not "Redis Never Fails"
Infrastructure will fail.
Networks become unreliable. Instances disappear. Services become slow. Deployments introduce bugs. Dependencies experience outages.
The goal of resilient architecture is therefore not:
Prevent every failure.
It is:
Limit the impact of failure and recover predictably.
For Redis, that usually means:
Redis healthy
│
▼
Fast cache responses
│
│ failure
▼
Bounded timeout
│
▼
Fallback to database
│
▼
Protect database from overload
│
▼
Recover Redis
│
▼
Resume normal caching
A cache should make your application faster, not make its availability dependent on the cache.
That is the architectural difference between simply adding Redis and actually designing a resilient backend.
Top comments (0)