jonnify·redis /redis-patterns/production-operations/kernel-tuning/latency-spikes

R8.01.2 · Kernel tuning — dive 2

Keeping the single command thread fast.

Every command runs on one thread. So anything that stalls it — a swapped page, a big synchronous free, a fragmented heap — is a latency spike. The fix is the same each time: push that work off the command thread.

Grounding: infra/valkey/conf/valkey.conf (the sizing premise, the threads, lazy frees, activedefrag, the observability block, verbatim) · the source kernel-tuning guide (swap, fragmentation bands) · bcs.8.md §B8.2 (the swappiness trade).

Where does the work run? Pick a latency source to see whether it lands on the command thread or is moved to the second vCPU — and which setting moves it.
command threadvCPU 1 · one command at a time helper threadsvCPU 2 · fsync, purge, frees, defrag swapped page → stalls big delete → lazyfree fragmentation → defrag fork on save → helpers
readout · Select a latency source. One lands on the command thread; the rest are moved to the second vCPU by a setting in valkey.conf.

Static verdicts — swapped page → on the command thread, fixed in the kernel (vm.swappiness=1) · big delete → moved off by lazyfree-lazy-* · fragmentation → kept down by activedefrag on the helper threads · fork on save → on the helper threads; the copy-on-write spike is dive 1.

§1

The single command thread

valkey.conf · the sizing premise

A Valkey server executes commands on one thread. That is the design, not a limit to fix: a single thread means a command runs to completion with no lock and no other command interleaved, which is what makes one SET or a Lua script atomic. The cost of the design is that the one thread is a shared resource — whatever it spends a millisecond on is a millisecond no command runs.

The committed tuning surface states the premise plainly. The box is a Fly shared-cpu-2x: command execution takes the first vCPU, and the second is reserved for the background helpers — AOF fsync, fork copy-on-write, jemalloc purge, lazy frees, active defrag — so none of that work lands on the command thread.

infra/valkey/conf/valkey.conf · the sizing premisefrozen
# Sizing premise: command execution is single-threaded, so the second shared
# vCPU is reserved for background work (AOF fsync, fork copy-on-write, jemalloc
# purge, lazy frees, active defrag) — not for busy-waiting I/O threads, which
# would burn the shared-CPU budget.

The rest of this dive is the catalogue of what can stall the command thread, and the one setting that moves each off it.

§2

Swap is the worst spike

kernel-tuning.md.txt · Swappiness

Memory access is measured in nanoseconds; a disk read is measured in milliseconds. When the kernel swaps a page of the dataset out to disk, the next read of that page pays the disk cost — the source puts the slowdown at roughly 1,000,000×. A single swapped page can be enough to time a client out, and the stall arrives with no warning at the server.

The fix is a kernel setting, not a server setting. vm.swappiness lives in the Linux VM and is set with sysctl — it is not a line in valkey.conf:

# the Linux kernel, not the server config
sysctl vm.swappiness=1

The source is specific about the value: 1, not 0. Zero forbids swap entirely, which can turn memory pressure into an OOM kill; one makes swapping extremely unlikely while still letting the kernel swap in a genuine emergency rather than killing the process. The manuscript states the same trade in one line: swappiness is set to its lowest practical value, because a swapped page trades nanoseconds for milliseconds.

A swapped page is the one spike on this page that lands directly on the command thread and is fixed outside the server. Everything after it is work moved off the thread by a valkey.conf setting.

§3

Keep work off the thread

valkey.conf · threads + lazy freeing

Three settings move steady-state work off the command thread and onto the second vCPU.

io-threads 1 on a shared-CPU box. Extra I/O threads busy-wait — they spin rather than sleep, which pins the CPU near 100% and drains a shared machine’s burst balance even while idle. The background helper threads (fsync, jemalloc background purge) sleep between cycles, so they are the right tenants for the second vCPU; the I/O threads are not.

Lazy frees (lazyfree-lazy-*). Freeing a large value is itself work, and done inline it runs on the command thread. The lazy-free settings hand that work to a background thread instead: an eviction, an expiry, a server-side delete, a user DEL, and a FLUSH each return immediately and reclaim the memory off-thread, so a single big delete does not stall the commands behind it.

jemalloc-bg-thread yes. jemalloc’s background purge thread sleeps between cycles and returns unused pages to the OS off the main thread, so reclaiming memory never costs a command.

infra/valkey/conf/valkey.conf · threads + lazy freeingfrozen
# ---- Lazy freeing: keep deletes off the command thread -----------------------
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
lazyfree-lazy-user-del yes
lazyfree-lazy-user-flush yes

# ---- Threads -----------------------------------------------------------------
# One I/O thread on a shared-CPU machine. Extra I/O threads busy-wait, which
# pins CPU near 100% and drains the shared burst balance even while idle. The
# background helper threads (fsync, jemalloc bg purge) sleep, so they are the
# right tenants for the second vCPU.
io-threads 1

# jemalloc's background purge thread sleeps between cycles; let it return pages
# to the OS off the main thread.
jemalloc-bg-thread yes

Notes on ValkeyThe fork that an AOF rewrite needs is its own latency source — the copy-on-write spike during the fork is the subject of dive 1, not this one; here the point is only that the fork runs on the helper threads, not the command thread — valkey.io/topics/latency.

One I/O thread, lazy frees, and the jemalloc background thread keep the second vCPU busy with the slow work so the command thread keeps running commands.

§4

Fragmentation and defrag

kernel-tuning.md.txt · Memory Allocator

Valkey uses jemalloc, well-suited to its allocation patterns, with one behaviour to watch. After a large amount of data is deleted, jemalloc retains the freed memory to satisfy future allocations rather than returning it all to the OS — so process RSS reflects peak usage, not the current dataset size. Over time the live objects can scatter across sparse runs, and the allocator works harder to place each new value: fragmentation.

The number to watch is mem_fragmentation_ratio in INFO MEMORY. The source gives three bands:

below 1.0
The process is using swap — critical. This is the §2 spike, seen from the allocator’s side.
1.0–1.5
Normal.
above 1.5
Significant fragmentation.

MEMORY PURGE forces jemalloc to return memory to the OS on demand. Better, the server can keep the ratio near 1 continuously: activedefrag yes relocates live objects out of sparse runs so empty pages are purged. The committed config pairs it with gentle cycle bounds — active-defrag-cycle-min 1 and active-defrag-cycle-max 25 — so defrag never takes more than a small slice of any cycle and the main thread is never starved.

infra/valkey/conf/valkey.conf · active defragmentationfrozen
# ---- Active defragmentation (jemalloc only) ----------------------------------
# Relocates live objects out of sparse jemalloc runs so empty pages are purged,
# keeping mem_fragmentation_ratio near 1. Gentle cycle bounds so the main thread
# is never starved.
activedefrag yes
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10
active-defrag-threshold-upper 100
active-defrag-cycle-min 1
active-defrag-cycle-max 25
active-defrag-max-scan-fields 1000

Fragmentation is the slow stall: not one spike but the allocator working a little harder on every write. Active defrag pays it down on the helper threads, with cycle bounds that never starve the command thread.

§5

Observing it

valkey.conf · the observability block

A spike you cannot see is a spike you cannot fix. The committed config turns on three observation surfaces:

slowlog-log-slower-than 10000
Record any command that runs longer than 10,000 µs (10 ms) in the slow log.
latency-monitor-threshold 100
Arm the latency monitor at 100 ms so the server tracks the events that crossed it.
latency-tracking yes
Keep per-command latency percentiles.

One caveat the config calls out: on Valkey 9.1, watch used_active_time_main_thread (via INFO) for true headroom — with I/O threads the raw CPU figure busy-waits near 100% and hides spare capacity, so the main-thread time is the honest number.

infra/valkey/conf/valkey.conf · the observability blockfrozen
# ---- Observability -----------------------------------------------------------
# On 9.1 watch used_active_time_main_thread (via INFO) for true headroom: with
# I/O threads the raw CPU figure busy-waits near 100% and hides spare capacity.
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 100
latency-tracking yes

Notes on ValkeyDiagnosing latency is a topic in its own right — fork pauses, fsync, huge pages, and slow commands each leave a different signature, and the latency monitor names which one fired — valkey.io/topics/latency.

the application · the second vCPU, sized in valkey.conf

The whole tuning surface, in one premise

The settings on this page are not chosen one by one — they follow from a single sizing premise that the committed config writes down at the top: one thread runs commands, the second vCPU carries everything slow, and the honest headroom is the main-thread time.

infra/valkey/conf/valkey.conf · one thread runs commands, the second vCPU carries the restfrozen
command execution     single-threaded   ← one command at a time, atomic
second vCPU           background work   ← fsync, COW, purge, frees, defrag
io-threads            1                 ← extra I/O threads busy-wait
true headroom         main-thread time   ← not the busy-waiting CPU figure
the pattern

Keep the single command thread fast: a swapped page, a synchronous free, and a fragmented heap are each a latency spike, so move that work off the thread — and watch the thread’s own time, not the busy-waiting CPU.

its application in valkey.conf

io-threads 1, lazyfree-lazy-*, jemalloc-bg-thread, and activedefrag move the work to the second vCPU; slowlog / latency-monitor-threshold / latency-tracking watch it; on 9.1 used_active_time_main_thread reads the honest headroom.

The command thread is the one resource every client shares. Each tuning move here is the same idea applied to a different stall — get the slow work off the thread, and measure the thread.

§6

Recap — off the thread, then measure it

next → R8.01.3

A latency spike on a single-threaded store is almost always work that landed on the command thread. Swap is the worst case and the cheapest fix (vm.swappiness=1, in the kernel). Lazy frees, one I/O thread, and the jemalloc background thread keep steady-state work off the thread; active defrag keeps the heap from forcing the allocator to work; and the slow log plus the latency monitor make a spike visible. The next dive turns to durability — AOF, noeviction, and the connection settings that keep a burst of clients from being dropped.

§7

References

sources · related

Sources

Related in this course