Redis Command Timeout — Separate Slow Commands from Network Delays
Track down Redis client timeouts by separating blocking commands from network and pool problems using SLOWLOG, latency probes, and INFO statistics instead of raising timeouts.
Symptoms
- Timeout performing GET, or Timeout reading from socket
- Timeouts cluster under load while single requests succeed
- P99 latency spikes in the application but the Redis CPU is low
- Failures disappear when the client timeout is increased
Possible causes
- A blocking command such as KEYS, SORT, or a large Lua script occupies the single command thread
- Large values or huge collections inflate serialization and network transfer time
- Connection pool wait time is counted against the command timeout
- A fork for RDB or AOF rewrite pauses the server under memory pressure
- Packet loss or retransmits between the client and Redis exceed the socket timeout
Diagnosis steps
Measure round-trip latency from the client host
redis-cli -h 127.0.0.1 -p 6379 --latency-history -i 5Interpretation
Stable sub-millisecond samples mean the server and local network are healthy, so investigate the client pool, event loop, or remote hop.
Read the slow command log
redis-cli -h 127.0.0.1 -p 6379 SLOWLOG GET 10Interpretation
Entries with large durations point to blocking commands. If the log is empty, raise slowlog-log-slower-than below the client timeout and reproduce.
Find commands with abnormal average cost
redis-cli -h 127.0.0.1 -p 6379 INFO commandstatsInterpretation
A high usec_per_call on GET or HGETALL indicates large payloads, not CPU-heavy logic.
Check memory, eviction, and fork pauses
redis-cli -h 127.0.0.1 -p 6379 INFO memory | grep -E 'used_memory_human|maxmemory_policy'; redis-cli -h 127.0.0.1 -p 6379 INFO stats | grep -E 'evicted_keys|expired_keys'; redis-cli -h 127.0.0.1 -p 6379 INFO persistence | grep -E 'latest_fork_usec|rdb_last_bgsave_status'Interpretation
Active eviction increases latency, and a multi-hundred-millisecond fork pause explains synchronized timeouts.
Root cause
The client gave up before the response arrived. Redis executes commands on one thread, so one slow command or fork pause delays every other client. When latency is normal on the server, the time is being spent in the client pool queue, serialization of large values, or the network path rather than in Redis itself.
Fix
Replace KEYS with SCAN, split large hashes and sets, pipeline or batch round trips, and move heavy aggregation out of Redis. Size the connection pool for the real concurrency, enable a bounded command timeout with jittered retries for idempotent reads, and alert on slowlog entries and evictions. Raise timeouts only after the slow source is removed.
Risk note
Retrying non-idempotent commands such as INCR or LPUSH duplicates writes. Raising the timeout without fixing the blocking command grows the client queue and turns timeouts into an outage. Disabling the timeout can hang request handlers indefinitely.
Verify the fix
Run the production load profile and confirm SLOWLOG stays empty, --latency-history is flat, evicted_keys stops increasing, and application P99 is well below the configured timeout for an extended run.
The timeout is a client verdict
A Redis client timeout means the reply did not arrive in time, not that Redis is down. Redis executes commands on a single thread, so one expensive command delays every other connection behind it. Start by measuring the server from the client host and reading the slow log.
redis-cli -h 127.0.0.1 -p 6379 --latency-history -i 5
redis-cli -h 127.0.0.1 -p 6379 SLOWLOG GET 10
redis-cli -h 127.0.0.1 -p 6379 INFO commandstats
If --latency-history is flat and SLOWLOG is empty, the time is being spent in the client pool, in serializing large values, or on the network hop. usec_per_call from INFO commandstats exposes expensive GET or HGETALL calls that are large rather than CPU-bound.
Rule out server pauses
Active eviction and a fork for persistence both add latency without showing up as slow commands. Check evicted_keys, used_memory against maxmemory, and latest_fork_usec. Synchronized timeouts across many clients usually mean a fork pause or a blocked command, not independent network faults.
Fix the source, then bound the wait
Replace KEYS with SCAN, split large collections, and pipeline independent reads. Size the pool for the real concurrency so checkouts do not queue behind a socket timeout.
import Redis from 'ioredis';
const redis = new Redis({
host: '127.0.0.1',
port: 6379,
commandTimeout: 500, // fail fast, do not queue forever
maxRetriesPerRequest: 2, // only for idempotent reads
enableOfflineQueue: false,
});
Keep a retry budget with jitter and only retry reads. An INCR that timed out may already have executed.
Verify under load
Replay the production load profile and watch SLOWLOG, latency history, evictions, and application P99 together. A fixed timeout should no longer be reachable under normal traffic, and no new slow commands should appear during the run.