← Debugging Guides

阅读中文版本

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.

intermediatedebuggingredisperformancetimeouts

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 5

Interpretation

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 10

Interpretation

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 commandstats

Interpretation

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.