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).
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.
The single command thread
valkey.conf · the sizing premiseA 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.
# 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.
Swap is the worst spike
kernel-tuning.md.txt · SwappinessMemory 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.
Keep work off the thread
valkey.conf · threads + lazy freeingThree 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.
# ---- 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.
Fragmentation and defrag
kernel-tuning.md.txt · Memory AllocatorValkey 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.
# ---- 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.
Observing it
valkey.conf · the observability blockA 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.
# ---- 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 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.
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
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.
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.
Recap — off the thread, then measure it
next → R8.01.3A 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.
References
sources · relatedSources
- Valkey — Diagnosing latency issues — the fork, fsync, swap, and slow-command signatures the latency monitor distinguishes.
- Valkey — Memory optimization — jemalloc’s high-water mark, fragmentation,
MEMORY PURGE, and active defragmentation. - Redis — Documentation — the memory and latency operations the source draws on:
mem_fragmentation_ratio, the slow log, and the latency monitor. - Sanfilippo, S. — antirez weblog — the Redis creator on the single-threaded core and why the one thread must stay free.
Related in this course
- R8.01 · Kernel tuningthe module hub
- R8.01.1 · Huge pages & overcommitthe fork-during-persistence spike
- R8.01.3 · Persistence-safe settingsthe next dive
- R8 · Production & Operationsthe chapter
- /bcs · Codemojex on Flythe same Valkey machine, from the inside