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
EventCauseMitigation
Periodic latency spikes with a save in progressFork for RDB or AOF rewriteMore memory headroom, less frequent saves, redirect writes
Latency after enabling AOFappendfsync alwaysUse everysec unless durability demands otherwise
Slow commands in SLOWLOGO(N) command on a large keySplit the key, use SCAN, move the work to a script with bounds
High CPU, low throughput with a large client countToo many round tripsPipeline or batch operations
Spikes correlated with a scheduled jobBig key deletionUse UNLINK instead of DEL
  • DEL on a large key frees memory synchronously and blocks the server; UNLINK hands 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 INFO rather than by issuing commands: a monitoring script that runs KEYS every 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

MetricSourceThreshold thinking
evicted_keysINFO statsAbove zero on a cache is expected; on a lock store it is an incident
keyspace_hits vs keyspace_missesINFO statsA hit ratio that drops suddenly means a deploy or an expiry bug
connected_clientsINFO clientsWatch for leaks: a client that never closes shows as a rising line
blocked_clientsINFO clientsA large number usually means a blocking command with a long timeout
latest_fork_usecINFO statsCorrelate with latency spikes
used_memory vs maxmemoryINFO memoryAlert well before the limit so eviction is not the first signal
rdb_last_bgsave_statusINFO persistenceA 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.

Distributed locks and rate limiting Clustering and client libraries in applications

Last refreshed 2026-09-18.