How to Build Near‑Zero‑Latency Logging in Rust for High‑Frequency Trading

The article walks through four incremental designs—from naïve println! logging to an enum‑based, zero‑allocation approach—showing how each step reduces latency in Rust, with benchmark numbers dropping from ~1769 ns to ~128 ns, and explains the reasoning, code, and trade‑offs.

Rust High-Frequency Quantitative Trading
Rust High-Frequency Quantitative Trading
Rust High-Frequency Quantitative Trading
How to Build Near‑Zero‑Latency Logging in Rust for High‑Frequency Trading

Performance Logging in Rust

Performance logging illustration
Performance logging illustration

In high‑frequency trading, every nanosecond matters, so logging must add virtually no overhead. The article examines how a simple logging implementation can be evolved into a Rust logging system that incurs almost no performance penalty.

1. Root Cause: Naïve Logging

Using the println! macro directly writes to standard output, causing blocking I/O and expensive formatting (e.g., date.format(...)) that allocates memory. A benchmark shows an average latency of about 1769 ns per log call, which is unacceptable in hot‑path code.

2. First Evolution: Asynchronous String Logging

The I/O block is removed by sending a pre‑formatted string to a dedicated logging thread via a lock‑free single‑producer‑single‑consumer channel.

// Asynchronous logging: send formatted string
use lockfree::channel::spsc;
use std::thread;
let (mut sx, mut rx) = spsc::create::<String>();
thread::spawn(move || {
    while let Ok(msg) = rx.recv() {
        println!("{}", msg);
    }
});
let date = Local::now();
let log_msg = format!(
    "ts: {} volume: {} price: {} flag: {}",
    date.format("%Y-%m-%d %H:%M:%S"),
    100.02,
    20000.0,
    true,
);
sx.send(log_msg).unwrap();

This reduces average latency to roughly 1189 ns , but the formatting work (the format! macro) still occurs on the strategy thread, leaving room for further optimization.

3. Second Evolution: Asynchronous Closure Logging

To move both I/O and formatting off the hot path, a closure that captures all required variables is created on the strategy thread and sent to the logging thread for execution.

// Asynchronous logging: send closure
use lockfree::channel::spsc;
use std::thread;
struct LogClosure {
    func: Box<dyn FnOnce() + Send + 'static>,
}
impl LogClosure {
    fn new<F>(func: F) -> Self
    where
        F: FnOnce() + Send + 'static,
    {
        Self { func: Box::new(func) }
    }
    fn invoke(self) {
        (self.func)();
    }
}
let (mut sx, mut rx) = spsc::create::<LogClosure>();
thread::spawn(move || {
    while let Ok(log_closure) = rx.recv() {
        log_closure.invoke();
    }
});
let date = Local::now();
let volume = 100.02;
let price = 20000.0;
let flag = true;
sx.send(LogClosure::new(move || {
    println!(
        "ts: {} volume: {} price: {} flag: {}",
        date.format("%Y-%m-%d %H:%M:%S"),
        volume,
        price,
        flag,
    );
})).unwrap();

Because the strategy thread no longer performs any formatting or allocation, the measured latency drops dramatically to about 170 ns , and tail latency improves noticeably.

4. Final Evolution: Enum‑Based Structured Logging

While the closure approach eliminates most overhead, it still incurs a heap allocation for the boxed closure and carries redundant format‑string data. The ultimate solution defines a fixed‑size struct for each log message and an enum that enumerates all possible messages. This enables stack‑only allocation and compile‑time known sizes.

use lockfree::channel::spsc;
use std::fmt;
use std::thread;
use chrono::{Local, DateTime};
struct SimpleLog {
    date: DateTime<Local>,
    volume: f64,
    price: f64,
    flag: bool,
}
enum LogMessage {
    Simple(SimpleLog),
    // TODO: more variants
}
impl fmt::Display for LogMessage {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            LogMessage::Simple(msg) => write!(
                f,
                "ts: {} volume: {} price: {} flag: {}",
                msg.date.format("%Y-%m-%d %H:%M:%S"),
                msg.volume,
                msg.price,
                msg.flag,
            ),
        }
    }
}
let (mut sx, mut rx) = spsc::create::<LogMessage>();
thread::spawn(move || {
    while let Ok(msg) = rx.recv() {
        println!("{}", msg);
    }
});
let date = Local::now();
sx.send(LogMessage::Simple(SimpleLog {
    date,
    volume: 100.02,
    price: 20000.0,
    flag: true,
})).unwrap();

This enum‑based method avoids heap allocation, keeps the data size fixed at compile time, and confines all formatting to the logging thread. Benchmarks show latency can be reduced further to around 128 ns , outperforming the closure variant.

Conclusion

The four‑step evolution—direct printing, async string sending, async closure sending, and async enum sending—demonstrates how Rust’s type system, ownership model, and zero‑cost abstractions enable developers to push logging latency down to the sub‑200 ns range without sacrificing safety. The analysis highlights the trade‑offs at each stage and provides concrete code that can be adapted to any low‑latency system.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

RustEnumLoggingAsyncHigh Performancelow-latencyClosure
Rust High-Frequency Quantitative Trading
Written by

Rust High-Frequency Quantitative Trading

Rust High-Frequency Quantitative Trading System

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.