Performance, pipelining and monitoring
Pipelining and batching, latency diagnosis, SLOWLOG, client-side caching, hot keys, fork stalls and the metrics worth alerting on.
Diagnosing latency
redis-cli --latency # continuous round-trip measurement
redis-cli --latency-history -i 5 # over time, shows spikes
redis-cli --intrinsic-latency 5 # how fast can this box run at all
redis-cli --stat # live ops/s, memory, clients
redis-cli SLOWLOG GET 10
redis-cli CONFIG SET slowlog-log-slower-than 10000 # 10 ms
redis-cli CONFIG SET latency-monitor-threshold 100 # 100 ms
redis-cli LATENCY HISTORY command
redis-cli LATENCY RESET
redis-cli INFO commandstats | sort -t= -k2 -rn | head| Event | Cause | Mitigation |
|---|---|---|
| Periodic latency spikes with a save in progress | Fork for RDB or AOF rewrite | More memory headroom, less frequent saves, redirect writes |
| Latency after enabling AOF | appendfsync always | Use everysec unless durability demands otherwise |
| Slow commands in SLOWLOG | O(N) command on a large key | Split the key, use SCAN, move the work to a script with bounds |
| High CPU, low throughput with a large client count | Too many round trips | Pipeline or batch operations |
| Spikes correlated with a scheduled job | Big key deletion | Use UNLINK instead of DEL |
DELon a large key frees memory synchronously and blocks the server;UNLINKhands the work to a background thread.- A fork for persistence copies page tables and can pause the server for a millisecond per gigabyte of memory. Keep instances smaller, or use replicas for persistence.
- Monitor with the server's own
INFOrather than by issuing commands: a monitoring script that runsKEYSevery minute is a self-inflicted incident.
💡
Redis is fast per operation, so most application-level slowness is round-trip count, not per-command cost. Measure commands per request before optimising anything else. One request issuing 300 sequential
GETs is a network problem, not a Redis problem.Pipelining and client-side caching
import redis
r = redis.Redis(decode_responses=True)
# pipelining: one round trip for 1000 writes
with r.pipeline(transaction=False) as pipe:
for i in range(1000):
pipe.hset(f"user:{i}", mapping={"name": f"user-{i}", "score": i})
pipe.execute()
# multi-key read in one round trip
with r.pipeline() as pipe:
for key in keys:
pipe.hgetall(key)
results = pipe.execute()
# client-side caching: the server pushes invalidations to the client
client = r.client_info()
cached = redis.Redis(decode_responses=True, protocol=3, cache_config=CacheConfig())
await cached.config_set("tracking-table-max-keys", 1000000)- A pipeline is not a transaction unless you use
MULTI; other clients can interleave between the commands. - Batch size matters: a 100k-command pipeline makes the client's reply buffer huge and delays other clients. A few hundred commands per pipeline is a good default.
- Client-side caching (RESP3 tracking) removes the round trip entirely for hot reads, and requires the client to honour invalidation pushes or it will serve stale data.
- Read from a replica to spread read load, but expect replication lag - never read a value you just wrote from a replica.
What to alert on
| Metric | Source | Threshold thinking |
|---|---|---|
evicted_keys | INFO stats | Above zero on a cache is expected; on a lock store it is an incident |
keyspace_hits vs keyspace_misses | INFO stats | A hit ratio that drops suddenly means a deploy or an expiry bug |
connected_clients | INFO clients | Watch for leaks: a client that never closes shows as a rising line |
blocked_clients | INFO clients | A large number usually means a blocking command with a long timeout |
latest_fork_usec | INFO stats | Correlate with latency spikes |
used_memory vs maxmemory | INFO memory | Alert well before the limit so eviction is not the first signal |
rdb_last_bgsave_status | INFO persistence | A failing background save is silent data-loss risk |
def report(client):
info = client.info()
stats = client.info("stats")
total = stats["keyspace_hits"] + stats["keyspace_misses"]
hit_ratio = stats["keyspace_hits"] / total if total else 0
print({
"hit_ratio": round(hit_ratio, 4),
"evicted": stats["evicted_keys"],
"clients": info["connected_clients"],
"mem_pct": round(info["used_memory"] / info["maxmemory"] * 100, 1) if info["maxmemory"] else None,
"bgsave_ok": info["rdb_last_bgsave_status"] == "ok",
})- Track the hit ratio per key pattern where your client supports it; a global ratio hides the one pattern that is missing every time.
- Alert on a change in the trend rather than a fixed number: evictions at 10 per minute mean something different from 10 per hour.
- Log the Redis version and configuration hash on every deploy, so a behaviour change can be traced to a settings change.
FAQ
Why is Redis suddenly slow when the load has not changed?
Check for a fork, an AOF rewrite, a big key deletion, or a client sending an O(N) command.
SLOWLOG and LATENCY HISTORY usually identify it in one look, and INFO commandstats shows which command got expensive.Should I run Redis and my application on the same host?
Only for development. In production a busy Redis competes for CPU and memory with the application, and a fork during a save doubles the memory pressure. Keep it on its own host or as its own container with explicit limits.
Related
Distributed locks and rate limiting Clustering and client libraries in applications
Last refreshed 2026-09-18.