JusDB
Free DB Audit
NoSQL Databases

A Latency Incident Runbook for Valkey 9: Find the Source of a Spike

Trace a Valkey 9.1 latency spike to its cause: client or server, COMMANDLOG, LATENCY DOCTOR and the 27 events 9.1.3 emits, I/O threads, fork, expiry, eviction, and swap.

JusDB Team
Published November 5, 2026
Updated November 10, 2026
19 min read

Your Valkey p99 has jumped from under a millisecond to 300 ms, the application logs are full of timeouts, and LATENCY LATEST shows an event called expire-del that the latency monitoring page never mentions. Valkey 9.1.3 is the current release (26 October 2026), but the official latency troubleshooting guide is still the Redis-era checklist. It tells you to read the "Slow Log", which Valkey 8.1 folded into COMMANDLOG. It never mentions I/O threads. Its event list names 16 events; the 9.1.3 source emits 27. This runbook covers the incident itself: confirm the delay is inside the server, find the code path blocking the main thread, and capture evidence before anything is reset. For configuration work outside an incident, read our general Valkey performance tuning guide instead.

TL;DR
  • Measure from the application host and the Valkey host first. The latency monitor and slow log time only main-thread execution.
  • Set latency-monitor-threshold (default 0, which means off) before the next incident. It records nothing retroactively; 10 ms matches the default slow log threshold.
  • In 9.1.3, LATENCY LATEST can report 27 event names. The documentation lists 16, and one of those, aof-rewrite-diff-write, is never emitted.
  • COMMANDLOG GET <count> slow|large-request|large-reply replaces the slow log. SLOWLOG GET and the slowlog-* settings still work as aliases for the slow type.
  • Capture INFO everything, the LATENCY output, and all three command logs before any LATENCY RESET, COMMANDLOG RESET, CONFIG RESETSTAT, or restart.

Scope and review date: November 2026. This runbook covers self-managed Valkey 9.1, where 9.1.3 is the current patch, and notes where 9.0 or 8.1 differ and where the 9.2 release candidate changes behavior. Event names, defaults, and output formats were checked against the Valkey documentation, release notes, and 9.1.3 source code on 10 November 2026. The commands were not run in a lab and the terminal output is illustrative, so rehearse on staging. Managed services may block some commands.


Is the latency coming from Valkey, the network, or the client?

The latency a client sees is the sum of time in its own connection pool, the network round trip, time in Valkey's I/O path, and command execution on the main thread. Valkey's own instruments see only the last part. The command latency event and the slow COMMANDLOG both time execution and exclude reading the request and writing the reply, as the COMMANDLOG GET reference states. So start by measuring the round trip from both ends.

bash
# 1. From an application host
valkey-cli -h valkey-01 -p 6379 --latency-history -i 5

# 2. From the Valkey host itself, against the same instance
valkey-cli -p 6379 --latency-history -i 5

Interpretation: both runs time PING round trips and print min, max, and average per interval (default 15 seconds). If the application host shows spikes and the local run stays flat, look at the network, TLS, or the client. If both spike together, the server is the bottleneck.

bash
valkey-cli INFO latencystats
valkey-cli INFO stats | grep -E 'total_connections_received|rejected_connections'

Interpretation: latency_percentiles_usec_<command> lines give p50, p99, and p99.9 of execution time per command since startup or the last CONFIG RESETSTAT. A flat server-side p99 while the client p99 climbs means the time is spent outside execution. A fast-rising total_connections_received points to a client reconnect storm.

On the client, check pool exhaustion, retry storms, and garbage-collection pauses. Valkey 9.0 turned on TCP_NODELAY by default for its own sockets (9.0.0-rc1 release notes, #1763); the client library sets its own.

Where a Valkey latency spike can come from

Only stages 3 to 5 show up in LATENCY LATEST; stages 1 and 2 need measurements from outside

Sources of Valkey request latency Stage 1, the client: pools, retries, and garbage collection. Stage 2, the network: round trips, TCP, and TLS. Stage 3, the main-thread event loop: commands, expiry, and eviction. From it, stage 4, fork with copy-on-write and huge pages, and stage 5, disk work such as AOF writes and nodes.conf saves. The latency monitor records stages 3 to 5 only. Client pool, retries, GC Network RTT, TCP, TLS Event loop main thread Fork CoW, huge pages Disk AOF, nodes.conf 1 2 3 4 5 1 and 2: measure outside

Rule of thumb: if a local --latency-history run on the Valkey host stays flat while the application host spikes, the cause is in stage 1 or 2, and no server setting will fix it.

What should you run first when Valkey latency spikes?

Work from the outside in, and read before you change anything. Only step 3 changes a runtime setting.

Valkey 9.1 latency incident runbook

Eight steps; only step 3 changes the server, and step 8 comes before any reset or restart

  1. Measure from both sides

    Run the same check from an application host and from the Valkey host. Different results mean the network or the client.

    valkey-cli -h valkey-01 --latency-history -i 5
  2. Check the host latency floor

    Run it on the Valkey host. It saturates one core and does not connect to Valkey.

    valkey-cli --intrinsic-latency 30
  3. Turn on the latency monitor

    The default threshold is 0, which means off. It only records spikes from now on.

    valkey-cli CONFIG SET latency-monitor-threshold 10
  4. Read the events

    The event name tells you which code path blocked the main thread.

    valkey-cli LATENCY LATEST valkey-cli LATENCY DOCTOR
  5. Read the three command logs

    Slow execution, large requests, and large replies are separate logs in 8.1 and later.

    valkey-cli COMMANDLOG GET 20 slow valkey-cli COMMANDLOG GET 20 large-reply
  6. Check background work

    Look at fork time, expiry that hit its time limit, and time spent over maxmemory.

    valkey-cli INFO stats | grep -E \ 'latest_fork_usec|time_cap|eviction_exceeded'
  7. Check threads and the host

    Read active time, not CPU percent, when I/O threads are on. Then check swap and huge pages.

    valkey-cli INFO cpu | grep used_active_time
  8. Capture everything

    Save the output to files before anyone resets counters, fails over, or restarts. The script is in the last section.

Main risk: diagnostics can add load. Never run KEYS during an incident, and run --bigkeys or --memkeys on a replica.

How do you read LATENCY LATEST, HISTORY, and DOCTOR in Valkey 9.1?

The latency monitor is off by default. Per the latency monitoring documentation, you enable it by setting latency-monitor-threshold in milliseconds. Each event at or above it is recorded in a 160-sample time series, with spikes in the same second merged by keeping the maximum. At 0 nothing is recorded, so an unconfigured instance has no history of the incident you are in.

bash
valkey-cli CONFIG SET latency-monitor-threshold 10
valkey-cli LATENCY LATEST
valkey-cli LATENCY HISTORY expire-del

Interpretation: since Valkey 8.1, each LATENCY LATEST entry has six fields: the event name, the Unix time of the latest spike, its latency in milliseconds, the all-time maximum, and the sum and count of recorded spikes (LATENCY LATEST; the page's Reply section still says four elements). LATENCY HISTORY returns the time and latency pairs for one event, so you can line spikes up with deploys, cron jobs, and BGSAVE times.

Choose a threshold below your latency budget. 10 ms matches the default slow COMMANDLOG threshold, and in the 9.1.3 source LATENCY DOCTOR warns when the slow log threshold is the higher of the two. Put latency-monitor-threshold 10 in valkey.conf so it survives restarts.

LATENCY DOCTOR turns the same data into a report with advice, and in 9.1.3 the advice lags the server. It says to check the "Slow Log" with slowlog-log-slower-than. It says to disable transparent huge pages with echo never, while the startup check recommends madvise. And it has advice only for the documented events, so newer events get statistics but no hint. All three are visible in the 9.1.3 latency.c source.

Which latency events does Valkey 9.1 emit that the docs do not list?

A search of the 9.1.3 source for latency samples finds 27 event names. The documentation lists 16. Fifteen match; aof-rewrite-diff-write is never emitted. The 9.2.0-rc2 source has the same 27, and modules can add their own.

Latency monitor events: documented vs emitted

Switch panels to compare the latency monitoring page with the Valkey 9.1.3 source

Documented

AreaEvents on the documentation page
Command executioncommand, fast-command
Fork and RDBfork, rdb-unlink-temp-file
AOFaof-write, aof-write-pending-fsync, aof-write-active-child, aof-write-alone, aof-fsync-always, aof-fstat, aof-rename, aof-rewrite-diff-write
Expiryexpire-cycle
Evictioneviction-cycle, eviction-del
Defragmentationactive-defrag-cycle
Cluster config fileNot listed
Blocked server and modulesNot listed
Total16 events

Emitted by 9.1.3

AreaEvents in the 9.1.3 source
Command executioncommand, fast-command, command-unblocking
Fork and RDBfork, rdb-unlink-temp-file
AOFaof-write, aof-write-pending-fsync, aof-write-active-child, aof-write-alone, aof-fsync-always, aof-fstat, aof-rename (no aof-rewrite-diff-write)
Expiryexpire-cycle, expire-del
Evictioneviction-cycle, eviction-del, eviction-lazyfree
Defragmentationactive-defrag-cycle
Cluster config filecluster-config-open, -write, -fsync, -close, -rename, -dir-fsync, -unlink
Blocked server and moduleswhile-blocked-cron, module-acquire-GIL
Total27 events, plus any module-defined events

An unfamiliar name is still real: LATENCY LATEST reports whatever the server recorded. Look the name up in the table below rather than dismissing it.

What the undocumented events time, from the 9.1.3 source:

EventWhat it timesWhat to check
expire-delDeleting one expired key from the keyspaceLarge keys with a TTL; lazyfree-lazy-expire
eviction-lazyfreeEviction waiting for the lazy-free thread after running out of keysMemory headroom, lazyfree_pending_objects
command-unblockingReply time when a client blocked by a module command is unblockedModule commands that block clients
while-blocked-cronHousekeeping while blocked in a script, module call, or data loadLong scripts or functions, loading after a restart
module-acquire-GILWaiting for module threads to release the module lockModules that run their own threads
cluster-config-*Saving nodes.conf on the main thread (added in 8.1, #1534)Disk latency during failovers or resharding

How do you find slow and oversized commands with COMMANDLOG?

Valkey 8.1 added COMMANDLOG (#1294 in the 8.1.0 release notes). It keeps three logs: slow for execution time, large-request for request size, and large-reply for reply size. In the 9.1.3 valkey.conf, commandlog-execution-slower-than defaults to 10000 microseconds, commandlog-request-larger-than and commandlog-reply-larger-than to 1048576 bytes, and each log keeps 128 entries.

A threshold of -1 disables a log and 0 logs every command. The old slowlog-log-slower-than and slowlog-max-len settings are aliases for the slow-type settings, and valkey.conf calls them deprecated. SLOWLOG GET still works and reads the same slow log, so existing dashboards keep working.

bash
valkey-cli COMMANDLOG GET 20 slow
valkey-cli COMMANDLOG GET 20 large-request
valkey-cli COMMANDLOG GET 20 large-reply

Interpretation: unlike SLOWLOG GET, which defaults to 10 entries, COMMANDLOG GET needs both a count and a type; -1 returns every entry. Each entry has six values: an ID, a Unix timestamp, the measured value (microseconds for slow, bytes for the other two), the command and its arguments, the client address, and the client name.

From LATENCY DOCTOR to the command that caused it

Illustrative output, shaped like Valkey 9.1.3; the numbers, key, and client are invented

sre@valkey-01:~
$ valkey-cli LATENCY DOCTORLatency spikes are observed in this Valkey instance.1. command: 4 latency spikes (average 212ms, mean deviation 61ms, period 38.25 sec). Worst all time event 290ms.2. expire-del: 2 latency spikes (average 14ms, mean deviation 3ms, period 120.50 sec). Worst all time event 17ms.Here is some advice for you:- Check your Slow Log to understand what are the commands you are running which are too slow to execute. ...$ valkey-cli COMMANDLOG GET 1 slow1) 1) (integer) 1187 2) (integer) 1794303672 3) (integer) 289514 4) 1) "HGETALL" 2) "session:index" 5) "10.0.4.17:51522" 6) "checkout-api"

Read across both: the doctor names the event (command) and still says "Slow Log"; in 9.1 that means COMMANDLOG GET <count> slow, where a 289,514-microsecond HGETALL on one key is the lead to follow. Formats are from the COMMANDLOG GET reference and latency.c.

The large-reply log has a cost with I/O threads

valkey.conf says that any commandlog-reply-larger-than value other than -1 adds tracking overhead when I/O threads are used. The 9.0.2 release notes use -1 to mitigate a 9.0.1 regression. If you disabled it for that reason, large replies are not being logged, so check the setting before trusting an empty log.

How do I/O threads change what you see during a spike?

With io-threads above 1, I/O threads read and parse requests, write replies, and poll sockets. Command execution stays on the main thread, as the latency monitoring page says. The count includes the main thread, so io-threads 1 means no extra threads. Since 9.0 it can be changed at runtime (#2033); avoid doing that mid-incident. Our Valkey 9.1 upgrade guide covers what changed in 9.1 threading.

Two consequences follow. Time a request waits for an I/O thread, or spends writing a large reply, is invisible to the latency monitor and the slow log. And CPU graphs mislead: the INFO reference explains that Valkey busy-waits when I/O threads are on, so CPU time looks close to 100% even with spare capacity. Valkey 9.1 added active-time counters for this.

bash
valkey-cli INFO cpu | grep used_active_time; sleep 10
valkey-cli INFO cpu | grep used_active_time

Interpretation: each field is seconds of real work since startup; subtract the samples and divide by 10. A main thread active for 9.5 of 10 seconds is saturated, and more I/O threads will not help. Busy I/O threads with an idle main thread point to network work, such as large replies or TLS.

Check the patch level too. 9.1.2 fixed a client deadlock with BLPOP followed by a partly delivered pipeline (#4531) and a frozen monotonic clock on hosts with unsynchronized TSC that stopped key expiration (#4346). 9.1.3 fixed event loop busy-spinning on VMs whose kernel clocksource is not tsc (#4707, 9.1.3 release notes). On a VM running 9.1.0 to 9.1.2, compare monotonic_clock in INFO server with the kernel clocksource; the fix is the upgrade.

Are big keys or blocking commands stalling the event loop?

One command on a large value blocks every other client for as long as it runs. The usual culprits are KEYS, and full reads of large collections such as HGETALL, SMEMBERS, or ZRANGE key 0 -1. Set operations like SUNION and SORT on large inputs are also common. They show up as the command event, in the slow log, and often in the large-reply log. BLPOP and similar commands only park one client.

bash
# Run on a replica: these scan the keyspace
valkey-cli -h valkey-replica-01 --bigkeys -i 0.01
valkey-cli -h valkey-replica-01 --memkeys -i 0.01
valkey-cli MEMORY USAGE session:index SAMPLES 0

Interpretation: --bigkeys reports the largest key per type by element count, and --memkeys by memory. -i 0.01 sleeps 10 ms every 100 scan cycles to reduce load. SAMPLES 0 measures every element, which is itself slow on a huge key.

Deleting big keys is less dangerous than it was. Since Valkey 8.0, lazy freeing is the default: lazyfree-lazy-eviction, lazyfree-lazy-expire, lazyfree-lazy-server-del, lazyfree-lazy-user-del, lazyfree-lazy-user-flush, and replica-lazy-flush are all yes in valkey.conf. Configurations carried over from older versions sometimes set them to no.

bash
valkey-cli CONFIG GET 'lazyfree-*'
valkey-cli CONFIG GET busy-reply-threshold

Interpretation: any no in the first result means deletes of that kind free memory on the main thread. busy-reply-threshold (alias lua-time-limit, default 5000 ms) is how long a script or function runs before other clients get a BUSY error. A while-blocked-cron event during the spike confirms the server was stuck in a long script, a module call, or a load.

Are expiry or eviction cycles causing the spikes?

Valkey removes expired keys when they are accessed and in an active expire cycle. valkey.conf describes the default active-expire-effort 1 (range 1 to 10) as keeping expired-but-present keys under about 10%. In the 9.1.3 source, at the default effort, the slow cycle may use up to 25% of CPU time and a fast cycle is capped at 1,000 microseconds. Since 9.0, the same cycle also reclaims expired hash fields. When many keys share one expiry time, such as a batch load with a fixed TTL, you see expire-cycle spikes.

bash
valkey-cli INFO stats | grep -E 'expired_|evicted_|eviction_exceeded'
valkey-cli INFO memory | grep -E '^used_memory:|^maxmemory:|^maxmemory_policy:'

Interpretation: a rising expired_time_cap_reached_count means active expiry cycles are stopping early on their time limit, and a high expired_stale_perc means expired keys are piling up. Spread TTLs with random jitter instead of raising the effort. On the eviction side, total_eviction_exceeded_time and current_eviction_exceeded_time (milliseconds) show how long used_memory has been above maxmemory.

Eviction runs before commands that can add memory whenever usage is over the limit, and appears as eviction-cycle and eviction-del. Its time budget comes from maxmemory-eviction-tenacity (default 10; valkey.conf describes 0 as minimum latency and 100 as no regard for latency). Lowering maxmemory suddenly forces mass eviction. For expiry spikes, LATENCY DOCTOR suggests hz 100 (default 10), which makes cycles smaller but more frequent, so test it first.

Is the host adding latency through fork, huge pages, or swap?

BGSAVE, AOF rewrite, and full resynchronization of a replica all fork. The fork() call runs on the main thread and copies the page table. The latency troubleshooting guide gives the example of a 24 GB instance needing a 48 MB page table. After the fork, each page the parent writes is copied (copy-on-write).

bash
valkey-cli INFO stats | grep latest_fork_usec
valkey-cli INFO persistence | grep -E 'bgsave_in_progress|rewrite_in_progress|_cow_size'

Interpretation: latest_fork_usec is the last fork's duration in microseconds, and the fork event records forks above the threshold. Spikes that line up with rdb_bgsave_in_progress:1 point to persistence. LATENCY DOCTOR also rates the fork rate in GB per second: under 10 is "terrible" and under 25 is "poor".

Transparent huge pages (THP) make copy-on-write far more expensive. Valkey ships disable-thp yes, which turns THP off for the Valkey process when the kernel is set to always, and the 9.1.3 startup check recommends madvise. The documentation and the 9.1.3 LATENCY DOCTOR still say echo never; the 9.2 release candidate changes the doctor to recommend madvise (#3947). Either value stops THP from applying to Valkey by default.

bash
cat /sys/kernel/mm/transparent_hugepage/enabled
pid=$(valkey-cli INFO server | awk -F: '/^process_id/{print $2}' | tr -d '\r')
grep -E '^(Swap|AnonHugePages):' /proc/$pid/smaps \
  | awk '{t[$1]+=$2} END {for (k in t) print k, t[k], "kB"}'
vmstat 1 5

Interpretation: non-zero AnonHugePages means the process is still using huge pages, and LATENCY DOCTOR notes that a restart is needed to drop pages that already exist. More than a few kB of Swap, or non-zero si and so columns in vmstat, means pages are moving to and from disk. The INFO reference adds that used_memory well above used_memory_rss means part of Valkey's memory has been swapped out. Then find the pressure behind it: other processes, fork copy-on-write growth, or a replica past maxmemory (9.1.3 fixed one cause, #4920).

What should you capture for the postmortem?

Most evidence lives in memory, and a restart, failover, LATENCY RESET, COMMANDLOG RESET, or CONFIG RESETSTAT erases it. Capture first, then fix. Save host data such as vmstat output alongside.

bash
#!/usr/bin/env bash
# Run against the affected node; add -h/-p/--user as needed.
out="valkey-incident-$(date -u +%Y%m%dT%H%M%SZ)"; mkdir -p "$out"
cli() { valkey-cli "$@"; }
cli INFO everything            > "$out/info.txt"
cli INFO debug                 > "$out/info-debug.txt"
cli LATENCY LATEST             > "$out/latency-latest.txt"
cli LATENCY DOCTOR             > "$out/latency-doctor.txt"
for e in $(cli --raw LATENCY LATEST | awk 'NR % 6 == 1'); do
  cli LATENCY HISTORY "$e"     > "$out/latency-history-$e.txt"
done
for t in slow large-request large-reply; do
  cli COMMANDLOG GET -1 "$t"   > "$out/commandlog-$t.txt"
done
cli CONFIG GET '*'             > "$out/config.txt"

Interpretation: INFO everything includes the latencystats and module sections. INFO debug holds eventloop_duration_max and related event-loop counters, which are not in the default output. The history loop reads every sixth raw line as an event name, so check its files. Command logs hold key names and client addresses, so store the folder like production data.

In the write-up, record the version, the event names and worst values, the top command log entries, fork times, and the threshold in force, lined up against client-side graphs. If the server shows nothing during a client-visible spike, say so: that moves the investigation to the network or the client.

Frequently asked questions

Is SLOWLOG deprecated in Valkey 9?

Not removed. Valkey 8.1 added COMMANDLOG, and SLOWLOG GET now reads the slow-type command log. The slowlog-log-slower-than and slowlog-max-len settings are aliases for commandlog-execution-slower-than and commandlog-slow-execution-max-len, and valkey.conf marks them deprecated. Existing scripts keep working.

Why does LATENCY LATEST show events that are not in the Valkey documentation?

The latency monitoring page lists 16 events, but the Valkey 9.1.3 source emits 27. The extra names are expire-del, eviction-lazyfree, command-unblocking, while-blocked-cron, module-acquire-GIL, and seven cluster-config events for saving nodes.conf. They are real main-thread measurements. The documented aof-rewrite-diff-write is never emitted.

What latency-monitor-threshold should I set for Valkey?

Set it below your latency budget. 10 ms is a practical start because it matches the default slow command log threshold of 10,000 microseconds. The default is 0, which disables monitoring, and nothing is recorded retroactively, so set it in valkey.conf before an incident.

Why is Valkey CPU near 100% with I/O threads enabled?

Valkey busy-waits when I/O threads are enabled, so CPU time looks close to 100% even when there is spare capacity. Valkey 9.1 added used_active_time_main_thread and used_active_time_io_thread_N to INFO cpu. Sample them twice and divide the difference by the interval to see how busy each thread really is.

Key takeaways

  • Measure from the client host and the Valkey host first; server tools see only main-thread execution.
  • Set latency-monitor-threshold in valkey.conf now. With the default of 0 there is no history when the next incident starts.
  • Treat unfamiliar event names in LATENCY LATEST as real. 9.1.3 emits 27 events, and LATENCY DOCTOR has advice only for the older ones.
  • Read all three command logs, and confirm commandlog-reply-larger-than is not -1 before trusting an empty large-reply log.
  • Capture INFO, LATENCY, COMMANDLOG, and CONFIG output to files before any reset, failover, or restart.

Get help with Valkey latency with JusDB

JusDB's Valkey performance tuning team can review your latency monitor and command log data after an incident, check I/O threads, persistence, and host settings against your workload, and help you write a latency runbook for your own topology.

Bring the capture folder from the last section, your Valkey version, and your client-side latency graphs. Your team approves every production change.

Share this article

Database engineering notes

Articles like this one, in your inbox. No spam, unsubscribe anytime.

JusDB Team

Official JusDB content team

Keep reading

Moving a Multi-Database Valkey Instance into a 9.x Cluster

Move a Valkey instance that uses SELECT 1 to 15 into a 9.x cluster: keep or remap databases, copy each one, verify counts per database, and keep a way back.

Redis18 minNov 10, 2026
Read

Valkey Keyspace Notifications in Production: Expired Events, hexpired and Cluster Mode

Run Valkey keyspace notifications in production: why expired and hexpired events lag the TTL, how to subscribe on every cluster node, and when a stream is safer.

Redis20 minNov 10, 2026
Read

Valkey GLIDE in Production: AZ-Affinity Reads and Migrating from Legacy Clients

Configure Valkey GLIDE AZ-affinity reads on servers and clients, learn what GLIDE 2.5.3 does not support, and migrate from redis-py, Jedis, ioredis, or go-redis one service at a time.

Redis20 minNov 10, 2026
Read