Skip to content

Host & OS Latency Parameters

The Parameter Reference covers Aeron’s transport knobs. This page is the layer underneath: the host, OS, and JVM settings that decide whether those transport knobs ever get to run on a quiet, predictable core. Almost every multi-millisecond tail that survives correct buffer sizing traces back to one of these — a core dropping into a deep C-state, a clock that sags under load, khugepaged collapsing pages, or an IRQ landing on a remote NUMA node.

Two things matter for every system knob beyond its latency effect: where it lives and how long it lasts. The persistence model is the part people get wrong.

Persistence classSet viaSurvives reboot?Notes
GRUB / kernel cmdlinegrubby --update-kernel=ALL --args=... then reboot✅ yesThe only class that requires a reboot to apply. On BLS-based distros (Amazon Linux 2023, recent Fedora/RHEL) grubby is authoritative — editing /etc/default/grub alone is not enough.
sysfs / proc (live)echo ... > /sys/...❌ no — resets on bootApplies instantly to the running kernel. No reboot, no process restart. Lost on reboot unless re-applied.
systemd unit / tuned profilea unit/profile that writes the sysfs value at boot✅ yesThe reboot-free way to make a live sysfs change persistent — apply live now, let the unit re-apply on every boot.
process flaglaunch script / service args✅ yes (re-read each start)e.g. JVM flags. Persistent because the launcher re-execs them — but needs a process restart, not a machine reboot, to take effect.

The single biggest source of µs-to-ms jitter on an otherwise idle-looking box is the CPU changing power state. A core that has dropped into a deep idle state takes microseconds to wake; a clock that ramps on demand adds variance to every first message after a quiet gap.

ParameterThroughputp50p99 (tail)Set viaPersistRevert
C-states (intel_idle.max_cstate=1, processor.max_cstate=1)NegligibleSmall win — cores don’t sleep between burstsLarge win — eliminates deep-sleep wake latency on the first packet after an idle gapGRUB cmdline✅ rebootgrubby --remove-args=... + reboot
CPU governor (performance)Keeps clocks high under loadLower baselineRemoves governor ramp lagcpupower frequency-set -g performance (sysfs)❌ liveset back to schedutil/ondemand
P-state driver mode (intel_pstate=passive / amd_pstate=guided)n/an/aLets the governor — not opaque HWP — own the clock, so a pin actually holdsGRUB cmdline✅ rebootremove arg + reboot
Frequency pin (scaling_min_freq = scaling_max_freq)Slightly below peak burstDead-flat baselineRemoves clock variance entirely — every core at one frequencysysfs per-CPU, or cpupower frequency-set -d F -u F❌ livewrite cpuinfo_min/cpuinfo_max back
Turbo / boost (intel_pstate/no_turbo, cpufreq/boost)Higher single-core peakn/aTurbo raises peak but adds variance; disabling pins to base (lower, but dead-uniform)sysfs❌ livere-enable (no_turbo=0 / boost=1)

The trap: single-core turbo ≠ all-core frequency

Section titled “The trap: single-core turbo ≠ all-core frequency”

A datasheet “max turbo” (e.g. 3.8–4.5 GHz) is a single-core number — reachable only when most cores are idle so the package has thermal/power headroom. The instant you load all cores, the hardware forces everyone down to the lower all-core turbo bin. So “maximum GHz” and “uniform GHz under load” are mutually exclusive: you cannot have every core at the single-core peak simultaneously.

For latency work you almost always want uniform over maximum. A clock that is flat across all cores — even at a lower value — removes a whole class of cross-core variance. The recipe:

  1. Release any pin and let the box find its natural all-core bin under full load (measure it — next section).
  2. Pin scaling_min_freq = scaling_max_freq at or just below that measured bin, so no core ever has to sag.

Confirming a flat all-core clock (detect + stress)

Section titled “Confirming a flat all-core clock (detect + stress)”

This is the verification step people skip — and then a “pinned” box quietly runs three different clocks across its cores. The method is: drive every core to 100% busy, then read the actual per-core frequency and check the spread.

Terminal window
# What the policy is set to (one line per unique min/max pair — expect exactly one):
paste \
<(cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_min_freq) \
<(cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq) | sort -u
# Driver + governor + turbo state:
cat /sys/devices/system/cpu/cpufreq/policy0/scaling_driver # intel_cpufreq | acpi-cpufreq | amd-pstate
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # expect: performance
cat /sys/devices/system/cpu/intel_pstate/no_turbo 2>/dev/null # 1 = turbo off

The requested pin is necessary but not sufficient — the SKU may not honor it under all-core load.

Step 2 — stress every core and read the achieved frequency

Section titled “Step 2 — stress every core and read the achieved frequency”

turbostat reports Bzy_MHz — the actual busy frequency each core ran at, which is the number that matters. The cleanest way is turbostat’s own built-in stress subcommand, which loads every logical CPU and samples them in one self-contained command (no stress-ng install needed):

Terminal window
# Preferred — turbostat's built-in all-core load. Pins one busy thread per logical CPU for SEC seconds:
sudo turbostat stress --cpu "$(nproc)" -t 30
# Observe-only (current workload, no added load) with the columns that matter:
sudo turbostat --interval 2 --show Core,CPU,Busy%,Bzy_MHz,Avg_MHz,TSC_MHz,C1%,IPC,IRQ
# Portable fallback if turbostat lacks the stress subcommand — busy-loop per CPU:
for i in $(seq 1 "$(nproc)"); do yes > /dev/null & done
turbostat --interval 1 --num_iterations 10 --quiet
pkill yes # ALWAYS stop the load afterwards

Read two things in the output:

  • The aggregate row (label - in the CPU column) gives the all-core average Bzy_MHz.
  • Each per-core row gives that core’s Bzy_MHz. Compute min / avg / max across them.
flat if (max − min) / avg ≤ ~1%
ObservationVerdict
All cores within ~1% of each other and of the pingenuinely flat all-core — pin is honored
A handful of cores sag below the rest under load⚠️ pin is above the all-core bin → lower the pin to the sagging floor
Every core runs below the requested scaling_max_freq🔴 hard SKU ceiling — the all-core bin is below your ask; pin at the achieved value, not the requested one
Wide spread only in the first 1–2 samples, then convergesramp lag on a cold box, not a real difference — warm up ≥5 s before judging

This same loop is how you discover the natural all-core bin in the first place: run it with the pin released (scaling_max_freq = cpuinfo_max_freq), read the sustained all-core Bzy_MHz, then pin at or just under it.

ParameterThroughputp50p99 (tail)Set viaPersistRevert
SMT / Hyper-ThreadingMore logical CPUs for non-critical workNeutral if hot threads never share a coreTwo hot threads on one physical core (SMT siblings) contend → tail spikes; keep critical agents off siblings/sys/devices/system/cpu/smt/control (sysfs)❌ liveecho on > .../smt/control
Core isolation & pinningHigher sustainable max — no run-queue contentionLess context-switch costOne of the strongest tail levers — see Core Isolation & Pinningisolcpus/nohz_full (GRUB) + taskset/numactl (process)mixedper-method
Tickless + RCU offload (nohz_full=<cpus>, rcu_nocbs=<cpus>)n/an/aRemoves the periodic scheduler timer tick and RCU softirq callbacks from the listed CPUs → no recurring interruption of a busy-spin agent. Pair the CPU list with your pinned coresGRUB cmdline✅ rebootgrubby --remove-args=... + reboot
NUMA placement (threads + memory NIC-local)Removes cross-socket trafficCuts memory/PCIe traversal costLarge — avoids remote-NUMA penalty; the receiver-stall fix lives here. See NUMA & Cache Localitynumactl --cpunodebind --membind wrapping the processprocess restartdrop the wrapper
IRQ affinity (NIC IRQs → NIC-local cores)Keeps RX off busy coresLower interrupt-handling jitterStops NIC interrupts preempting the receiver agent/proc/irq/<n>/smp_affinity_list (sysfs)❌ liverestore previous mask
irqbalancen/an/aDaemon re-shuffles IRQs at runtime → periodic affinity drift → sporadic spikes; disable for pinned setupssystemctl disable --now irqbalancesystemctl enable --now irqbalance
kernel.numa_balancingn/aRemoves page-migration scan costStops automatic NUMA page migration from stalling hot threadssysctl / 99-*.conf✅ (file)set back to 1
ParameterThroughputp50p99 (tail)Set viaPersistRevert
Transparent HugePages (THP) modeFewer TLB misses on large heapsSlightly lower with hugepage-backed heapalways lets khugepaged collapse pages process-wide → periodic stalls; madvise is calmer; never is the most deterministic/sys/kernel/mm/transparent_hugepage/{enabled,defrag} (sysfs)❌ live (or GRUB transparent_hugepage=never)echo madvise > ...
JVM +UseTransparentHugePagesTLB win on the heapWith always/madvise the JVM madvises its heap into THP; disabling (-UseTransparentHugePages) takes the heap off THP entirelyJVM flag in launch scriptprocess restartflip flag back, restart
Explicit hugepage pool (nr_hugepages)Required for kernel-bypass (DPDK) mbuf poolsPre-reserved, never collapsed → no khugepaged jittersysfs per NUMA node❌ liveset pool back to 0
vm.swappiness=0Avoids reclaim stallsStops the kernel swapping hot pages under memory pressuresysctl✅ (file)default 60
vm.compaction_proactiveness=0Disables proactive background memory compaction — its page-migration scans steal cycles and fault hot pages mid-runsysctl✅ (file)default 20
vm.max_map_count (raise)Allows many mapped buffers/term filesPrevents mmap failures under many streamssysctl✅ (file)default

These overlap with the transport-layer receive-path buffers; here they’re the OS-level limits that cap what the application can request.

ParameterThroughputp50p99 (tail)Set viaPersistRevert
net.core.rmem_max / wmem_maxHeadroom to absorb bursts without dropsToo small → RX-ring/socket overflow → NAK recovery spikes; size to several × BDP for bursty fan-insysctl / 99-*.conf✅ (file)distro default (~212 KB)
net.core.netdev_max_backlogDeeper per-CPU ingress queuePrevents drops when packets arrive faster than the stack drains themsysctl✅ (file)default
net.ipv4.ip_local_reserved_portsn/an/aReserves the app’s control/data ports so the kernel never auto-assigns them to an ephemeral socket and collides — an availability/jitter gotcha, not a throughput knobsysctl✅ (file)clear the range
NIC ring buffers (e.g. ENA)Match rings to bandwidthCorrectly sized keeps queuing lowOversized rings add queue-bloat latency; undersized drop under burstsethtool -G❌ liveprior ring size
FlagEffectPersist
-XX:+UseZGC (or a low-pause collector)Sub-millisecond GC pauses → removes the GC contribution to the tailprocess flag
-XX:+AlwaysPreTouchFaults the whole heap at startup → no first-touch page-fault stall mid-run (pairs with hugepages)process flag
-XX:+UseTransparentHugePagesMadvises the heap into THP — see the THP note above for when to turn it offprocess flag
JIT prewarmDrive representative traffic through all hot paths before going live — cold code + first-touch cause severe early spikes. See Warming Up the JVMruntime procedure

For any cross-host latency measurement (one-way segment timing, phase-aligned bursts) the hosts must share a sub-microsecond time base, or your numbers are noise.

ParameterWhy it mattersSet viaPersist
PTP / chrony with hardware PHCDisciplines every host to a common ns-grade reference (e.g. a NIC PTP hardware clock) → honest one-way segment math and viable wall-clock burst alignmentchronyd with refclock PHC✅ service
LayerNeeds reboot?Needs app restart?Lost on reboot unless…
C-states, P-state driver mode, isolcpus, nohz_full, rcu_nocbs✅ (cmdline)no— (GRUB persists)
Governor, frequency pin, turbo, THP sysfs, IRQ affinity, NIC rings, SMTnonore-applied by a systemd unit / tuned profile
sysctl values (rmem_max, swappiness, numa_balancing, compaction_proactiveness, …)nonowritten to /etc/sysctl.d/*.conf
JVM flags, numactl process wrapper, systemd CPUAffinityno— (in the launch script / unit)
Explicit hugepage poolnono (but DPDK app needs it before start)re-applied at boot
  • Flatten the host before tuning transport. C-states limited, governor performance, a measured frequency pin, SMT siblings clear, IRQs and memory NIC-local. These attack variance at its source.
  • Know your persistence class. Only kernel-cmdline changes need a reboot; most knobs are live sysfs, and a few need an app restart but not a host reboot.
  • Never trust a frequency pin you haven’t stress-confirmed. Load all cores, read Bzy_MHz, gate on a ≤1% spread — the requested scaling_max_freq can silently differ from the achieved all-core clock.
  • THP is two layers (system mode + JVM flag) and changing it cleanly means touching both, then restarting the JVM so the live heap actually leaves THP.
  • Silence the recurring interrupters. Beyond pinning, the cores running busy-spin agents want nohz_full + rcu_nocbs (no timer tick / RCU softirq), IRQs steered elsewhere, and vm.compaction_proactiveness=0 — each removes a periodic source of jitter that a one-time pin can’t.

For the Aeron transport knobs that sit on top of this foundation, see the Parameter Reference and the tuning overview.