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 pin✅ genuinely 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 irqbalance✅systemctl 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 heap—With 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 pools—Pre-reserved, never collapsed → no khugepaged jittersysfs per NUMA node❌ liveset pool back to 0
vm.swappiness=0—Avoids reclaim stallsStops the kernel swapping hot pages under memory pressuresysctl✅ (file)default 60
vm.compaction_proactiveness=0——Disables 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 files—Prevents 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 drops—Too 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 queue—Prevents 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.

This site is not affiliated with, endorsed by, or sponsored by Adaptive Financial Consulting Limited or the Aeron project. Aeron is a registered trademark of Adaptive Financial Consulting Limited.

Aeron is a trademark of Adaptive Financial Consulting Limited in the United Kingdom and other countries.