Why Rust for Ultra-Low Latency Systems
Rust's pitch for distributed infrastructure isn't just "safe C++" — it's that the ownership model lets you write concurrent, allocation-conscious code without a garbage collector pausing your event loop at the worst possible moment, and without the class of memory-safety bugs that historically made lock-free C++ so dangerous to maintain. For systems where p99.9 latency matters — matching engines, real-time bidding, consensus layers — a GC pause of even a few milliseconds is a correctness-adjacent problem, not just a performance nuisance. This piece walks through the three building blocks that actually matter in practice: an async runtime tuned for the hot path, lock-free data structures for cross-thread handoff, and a consensus engine built for your workload rather than a generic off-the-shelf one.
Building the Async Event Loop
Tokio is the default choice, and for good reason — it's a work-stealing multi-threaded scheduler with a mature ecosystem. But the default configuration optimizes for throughput and fairness, not tail latency. For latency-sensitive services, a few adjustments matter:
- Pin the runtime to specific cores and avoid sharing them with the OS scheduler's default pool, so your async tasks aren't fighting unrelated processes for cache residency.
- Use a bounded worker thread count matching physical (not logical/hyperthreaded) cores for CPU-bound work, since hyperthreads share execution units and hurt tight numeric loops.
- Avoid
tokio::spawnchurn on the hot path — spawning a task allocates. Prefer a small set of long-lived tasks communicating over channels rather than spawning per-request.
use tokio::runtime::Builder;
fn build_runtime() -> std::io::Result<tokio::runtime::Runtime> {
Builder::new_multi_thread()
.worker_threads(num_cpus::get_physical())
.thread_name("io-worker")
.enable_all()
.on_thread_start(|| {
// Pin each worker to a core with core_affinity here
})
.build()
}
For the truly latency-obsessed, tokio-uring or monoio (a thread-per-core runtime built on io_uring) eliminate the syscall overhead of epoll's readiness model in favor of Linux's completion-based I/O, which matters a lot once you're pushing tens of thousands of small messages per second per connection.
Lock-Free Structures for the Hot Path
Mutexes are fine for coordination that happens rarely. For anything on the per-message critical path — handing work between an I/O thread and a processing thread, for instance — a lock introduces both blocking latency and priority inversion risk. The crossbeam crate is the practical toolbox here: crossbeam::queue::ArrayQueue gives you a bounded lock-free SPSC/MPMC ring buffer backed by atomics rather than a mutex.
use crossbeam::queue::ArrayQueue;
use std::sync::Arc;
let ring: Arc<ArrayQueue<Message>> = Arc::new(ArrayQueue::new(4096));
// Producer thread (e.g. network I/O)
if ring.push(msg).is_err() {
// Ring full: this is a backpressure signal, not an error to ignore.
// Decide explicitly: drop, block, or spill to a slower path.
}
// Consumer thread (e.g. processing loop)
while let Some(msg) = ring.pop() {
process(msg);
}
A common mistake is treating a full ring buffer as an edge case rather than a design decision. Bound your queues deliberately and decide up front what happens under backpressure — silently growing an unbounded queue just turns a throughput problem into a latency and memory problem later.
False Sharing Is the Silent Killer
Two atomics that happen to land on the same 64-byte cache line will ping-pong between cores even though logically unrelated, destroying the performance gain lock-free code was supposed to buy you. Pad hot fields explicitly:
#[repr(align(64))]
struct PaddedCounter {
value: std::sync::atomic::AtomicU64,
}
Or use crossbeam_utils::CachePadded<T>, which does this for you. Profile with perf c2c on Linux if you suspect this is happening — it directly reports cache-line contention between cores, which is otherwise nearly invisible in a normal flamegraph.
A Consensus Engine Tailored to the Workload
Generic Raft implementations (raft-rs, openraft) are good starting points, but "tailored" usually means adjusting a few specific knobs rather than writing Raft from scratch:
- Log batching: instead of committing one log entry per client request, accumulate entries for a short window (sub-millisecond to a few milliseconds) and replicate them as a single AppendEntries batch. This trades a small amount of added per-request latency for a large increase in achievable throughput, since network round-trips dominate the cost.
- Pipelining AppendEntries: don't wait for an ack before sending the next batch to a follower — track in-flight batches and let the leader keep pushing, bounded by a window size, similar to TCP's sliding window.
- Separating the log write path from the state machine apply path: fsync the log for durability before acknowledging a write, but apply to the in-memory state machine asynchronously once a quorum has acknowledged, so read-after-write consistency on the leader doesn't block on follower disk I/O.
- Pre-vote and leader leases: pre-vote avoids unnecessary term increases from a partitioned node rejoining and immediately triggering elections; leader leases let the current leader serve linearizable reads without a quorum round-trip for a bounded time window, at the cost of relying on bounded clock drift between nodes.
struct AppendBatch {
entries: Vec<LogEntry>,
deadline: tokio::time::Instant,
}
async fn batch_collector(
mut rx: tokio::sync::mpsc::Receiver<LogEntry>,
max_batch: usize,
max_delay: std::time::Duration,
) {
loop {
let mut batch = Vec::with_capacity(max_batch);
let deadline = tokio::time::Instant::now() + max_delay;
while batch.len() < max_batch {
match tokio::time::timeout_at(deadline, rx.recv()).await {
Ok(Some(entry)) => batch.push(entry),
_ => break,
}
}
if !batch.is_empty() {
replicate(batch).await;
}
}
}
Measuring What You Built
None of this matters without measurement discipline. Use hdrhistogram to record latency distributions rather than averages — averages hide exactly the tail behavior you're trying to fix. Record at the point closest to the actual client-observed latency (request received to response sent), not just internal processing time, and report p50/p99/p99.9 separately since they usually have different root causes: p50 tells you about steady-state efficiency, p99.9 usually tells you about GC-equivalent pauses (in Rust's case, allocator contention, page faults, or lock contention you missed).
perf stat with -e context-switches,cpu-migrations during a load test. High counts on either usually point directly at a runtime configuration problem — threads being scheduled off their expected core, or blocking calls sneaking into async tasks and stalling the executor.
Putting It Together
None of these three pieces is exotic in isolation — tuned async runtimes, lock-free ring buffers, and batched/pipelined Raft are all individually well-documented patterns. The actual engineering work in an ultra-low-latency Rust system is making sure the boundaries between them don't reintroduce the costs you eliminated: a lock-free queue feeding into a task that still calls a blocking function, or a beautifully pipelined Raft implementation whose fsync policy silently serializes every write. Profile the whole path, not the pieces.
Discussion & Insights