Redis is often introduced to make an application faster. Instead of querying the database for the same data repeatedly, the application reads frequently used data from memory.
That usually works well, but adding Redis does not automatically make every request faster. Redis itself can become the bottleneck when too much traffic, too many connections, large values, expensive operations, or inefficient cache usage are pushed through it.
This can be confusing during production troubleshooting. The database may show lower CPU usage and fewer queries, while API latency continues to increase. Looking only at the database can make the problem difficult to find.
The important point is that Redis is part of the request path. If the application depends heavily on Redis, problems in the cache layer can become application performance problems.
How Redis Becomes a Bottleneck
A typical cached request looks like this:
Client
|
v
API
|
v
Redis
|
+---- Cache hit ----> Return data
|
+---- Cache miss ---> Database
|
v
Redis
|
v
Return data
When Redis responds quickly, this architecture can reduce database load significantly.
The problem starts when Redis cannot keep up with the application's traffic.
For example, an API may receive thousands of requests per second. If most requests perform multiple Redis operations, the total number of Redis commands can become much larger than the request rate.
A single HTTP request might perform:
GET user
GET permissions
GET configuration
SET session
GET feature flags
One API request can therefore generate five Redis operations.
At high traffic levels, this difference matters.
Start by Checking Redis Latency
When Redis is suspected to be the bottleneck, begin with Redis itself rather than immediately changing the database.
Redis provides command-line tools and runtime statistics that can help identify latency problems.
For example:
redis-cli INFO stats
and:
redis-cli INFO commandstats
These provide information about command activity and statistics.
You can also inspect Redis latency using:
redis-cli LATENCY LATEST
The goal is to determine whether Redis is actually taking too long to respond or whether the delay is happening elsewhere in the application.
A slow API does not automatically mean Redis is slow.
Check CPU Usage
Redis is generally very efficient, but CPU can still become a limiting resource.
Check whether the Redis server is consistently using high CPU.
If CPU usage remains high during traffic spikes, investigate which commands are consuming the time.
The command statistics can help identify expensive or frequently executed operations.
For example, a problem might look like this:
GET 4,500,000 calls
HGET 3,800,000 calls
ZRANGE 900,000 calls
KEYS 50 calls
The number alone does not prove that a command is problematic. You need to consider both frequency and cost.
A command executed millions of times may be perfectly reasonable if each operation is cheap. A command executed a few thousand times can still cause trouble if it processes a large amount of data.
Large Values Can Make Redis Slow
One of the easiest Redis problems to overlook is value size.
Redis stores data in memory, so large values consume memory and also take longer to transfer between Redis and the application.
For example, storing a small user profile is usually inexpensive:
{
"id": 42,
"name": "John",
"role": "admin"
}
But storing a large serialized object containing thousands of records can create a very different workload.
If an application stores a 5 MB JSON document and retrieves it on every request, Redis may be saving a database query while creating a network and serialization bottleneck.
The application must perform:
Redis
|
| 5 MB response
v
Network
|
v
Application
|
v
JSON deserialization
At high request rates, this can become expensive.
Cache smaller pieces of data when appropriate instead of storing large application objects without considering their access pattern.
Too Many Redis Operations Per Request
Another common problem is excessive round trips.
Consider code that performs several Redis calls one after another:
var user = await database.StringGetAsync(userKey);
var permissions = await database.StringGetAsync(permissionKey);
var settings = await database.StringGetAsync(settingsKey);
var flags = await database.StringGetAsync(flagsKey);
Each operation can involve network communication.
The individual Redis commands may be fast, but the repeated round trips can increase application latency.
Where appropriate, use batching or pipelining so that multiple independent operations can be sent together.
For example, StackExchange.Redis supports batching through its asynchronous APIs and pipelining behavior.
The exact approach depends on the application and the client library, but the general principle is the same:
Reduce unnecessary network round trips.
Connection Pooling Can Also Become a Problem
Redis clients manage connections differently from traditional relational database connection pools.
A common mistake is creating a new Redis connection for every request.
For example, avoid patterns like:
public async Task<string?> GetValue(string key)
{
using var connection = await ConnectionMultiplexer.ConnectAsync(connectionString);
var database = connection.GetDatabase();
return await database.StringGetAsync(key);
}
Creating connections repeatedly adds unnecessary overhead.
With StackExchange.Redis, a shared ConnectionMultiplexer is normally used and reused throughout the application's lifetime.
For example:
public sealed class RedisService
{
private readonly ConnectionMultiplexer _connection;
public RedisService(ConnectionMultiplexer connection)
{
_connection = connection;
}
public async Task<RedisValue> GetAsync(string key)
{
var database = _connection.GetDatabase();
return await database.StringGetAsync(key);
}
}
The important architectural decision is that the connection object should be managed as a long-lived shared resource rather than repeatedly created for individual requests.
Cache Stampede Can Overload Redis and the Database
A cache stampede happens when many requests try to rebuild the same missing cache entry at the same time.
Imagine a product cache expires:
Product:123
Now 5,000 requests arrive almost simultaneously.
All of them see a cache miss:
Request 1 -> Redis MISS -> Database
Request 2 -> Redis MISS -> Database
Request 3 -> Redis MISS -> Database
...
Request 5000 -> Redis MISS -> Database
The result can overload both Redis and the database.
A common solution is request coalescing or locking so that one request rebuilds the value while other requests wait for the result.
Another option is using carefully designed expiration strategies such as jittered expiration times.
For example, instead of every cached item expiring at exactly the same interval, expiration can be slightly randomized.
This reduces the chance of many keys expiring simultaneously.
Cache Hit Rate Is an Important Metric
A cache is useful only when it actually serves a meaningful amount of traffic.
Suppose an application performs one million cache reads.
If 950,000 are hits:
Hit rate = 95%
That is generally a strong cache hit rate.
But if only 300,000 requests are hits:
Hit rate = 30%
the application is still sending a large amount of traffic to the database.
A low hit rate can happen because of:
Before increasing Redis capacity, determine whether the application is using the cache effectively.
Memory Pressure Can Cause Evictions
Redis is an in-memory system, so available memory is an important resource.
When Redis approaches its configured memory limit, eviction policies may remove cached data.
This can cause a sudden increase in cache misses.
The pattern may look like:
Normal traffic
|
v
High memory usage
|
v
Keys evicted
|
v
Cache misses increase
|
v
Database traffic increases
This creates a feedback loop.
The database receives more requests because Redis has fewer useful cached values. The application becomes slower, and Redis continues handling a large amount of cache activity.
Monitor memory usage and eviction statistics together rather than looking at memory alone.
Slow Commands Can Block Redis
Redis command execution is an important area to understand.
Many Redis operations are fast, but some commands can become expensive when applied to large datasets.
Avoid using commands that scan large amounts of data in the request path unless their cost is understood and acceptable.
For example, using:
KEYS *
against a large production Redis instance can be dangerous because it can require Redis to examine a large number of keys.
For production key inspection, use cursor-based approaches such as SCAN when appropriate.
The broader lesson is that Redis being an in-memory database does not mean every operation has constant practical cost.
Network Latency Matters
Redis can be extremely fast when the application and Redis server are located close to each other.
If the application is running in one region and Redis is running far away, network latency becomes part of every cache operation.
For example:
Application Region A
|
| Network
v
Redis Region B
Even a small amount of additional latency becomes significant when an API performs many sequential Redis calls.
This is why Redis placement should be considered as part of application architecture.
For latency-sensitive applications, keep the application and cache deployment topology aligned.
Serialization Can Become a Hidden Cost
Redis stores bytes. The application is responsible for converting application objects into those bytes and back again.
For example:
Object
|
v
JSON serialization
|
v
Redis
|
v
JSON deserialization
|
v
Object
Large objects and high request volumes can make serialization a noticeable part of request time.
If Redis latency appears low but application latency remains high, measure the time spent serializing and deserializing cache values.
Do not assume every delay is coming from Redis itself.
How to Troubleshoot a Redis Bottleneck
A practical investigation can follow this order.
1. Check Application Latency
Determine which API endpoints are slow.
Do not start by assuming Redis is responsible.
2. Measure Redis Command Latency
Check Redis latency and command statistics.
Look for commands that are unusually slow or called excessively.
3. Check CPU and Memory
Inspect Redis CPU, memory usage, evictions, and network activity.
4. Check Cache Hit Rate
Determine whether the application is getting enough value from Redis.
5. Check Value Sizes
Look for unusually large cached objects.
6. Check Network Placement
Confirm whether the application and Redis are deployed close enough for the workload.
7. Check Client Usage
Look for unnecessary connection creation, sequential round trips, and inefficient client configuration.
8. Check Expiration Patterns
Look for many keys expiring at the same time.
9. Check Expensive Commands
Review commands that scan, sort, aggregate, or otherwise process large amounts of data.
10. Compare Redis and Database Metrics
The goal is to understand the entire request path.
API latency
|
+-- Redis latency
+-- Database latency
+-- Serialization time
+-- Network time
+-- Application processing
This prevents the team from fixing the wrong component.
Common Mistakes
Treating Redis as Infinitely Fast
Redis is fast, but hardware, network, memory, command complexity, and workload size still matter.
Creating Connections Per Request
Use the client library's recommended connection-management pattern instead of repeatedly creating Redis connections.
Caching Everything
Not every database query needs to be cached. Poor cache design can increase complexity without improving performance.
Ignoring Cache Misses
A cache with a poor hit rate may not provide enough benefit to justify its operational complexity.
Storing Large Objects Without Measurement
Large cache values increase memory usage, network traffic, and serialization cost.
Adding More Redis Capacity Too Early
Scaling Redis may hide an inefficient access pattern.
First determine whether excessive commands, large values, poor expiration behavior, or low cache hit rates are causing the problem.
Best Practices
Keep cache keys predictable and consistent across the application.
Set TTL values based on how frequently the underlying data changes rather than choosing an arbitrary expiration time.
Monitor cache hit rate, memory usage, evictions, command latency, command frequency, network traffic, and application latency together.
Use a shared Redis client connection rather than creating connections for individual requests.
Avoid large values when smaller cached objects can satisfy the same use case.
Reduce sequential Redis calls where batching or pipelining is appropriate.
Protect the cache from stampedes when expensive values expire.
Treat Redis as a production dependency and monitor it with the same seriousness as the database.
Summary
Redis can improve application performance, but it can also become the bottleneck when traffic, memory usage, command volume, network latency, or cache design gets out of control.
When Redis appears to be slowing the application, check command latency, CPU, memory, evictions, cache hit rate, value size, network placement, and client usage before simply adding more capacity.
The goal is not just to make Redis faster. It is to make the entire request path efficient.
Join the conversation! Your thoughts help the community grow.