9 min read

Taming the Chaos: Why We Built a Time-Traveling Simulator for Our HFT Consensus Engine

Time-Traveling Simulation for HFT Consensus Engines

Taming the Chaos: Why We Built a Time-Traveling Simulator for Our HFT Consensus Engine

It’s 2:14 AM. Your phone is screaming. A high-frequency trading (HFT) cluster in the Tokyo data center just suffered a partial network partition. For three milliseconds, Node A thought it was the leader, while Node B—due to a subtle clock drift and an interleaved disk I/O stall—decided it was also the leader. By the time the dust settled, your risk management engine had double-counted a $50 million position.

You check the logs. They are a mess of interleaved timestamps, missing heartbeats, and “Impossible State” errors. You try to reproduce it in the staging environment. You run the same workload ten times, a hundred times, a thousand times. Nothing. The bug is a ghost, a “Heisenbug” that only appears when the cosmic rays of network jitter and kernel scheduling align in a perfectly catastrophic sequence.

In the world of ultra-low latency distributed systems, “testing” is often a polite word for “praying.” But what if you could record the state of the entire universe, rewind it, and replay it with 100% bit-for-bit accuracy? What if you could compress years of production-level stress into a few hours of CPU time?

Welcome to the world of Deterministic Simulation Testing (DST). This is how we moved away from hoping for correctness to mathematically enforcing it.


The Distributed Consensus Crisis in HFT

In HFT, consensus engines (usually variants of Raft or Paxos) are the backbone of the system. They ensure that every trading node agrees on the state of the order book, the firm’s risk exposure, and the sequence of incoming market data.

The industry standard used to be simple: write your Raft implementation, throw a battery of unit tests at it, run Jepsen for a weekend, and call it a day. But HFT presents a unique challenge: the latency-correctness paradox. To achieve sub-microsecond consensus, we often bypass the kernel (using bypass stacks like Solarflare’s Onload or DPDK) and utilize hardware-level optimizations.

When you are operating at this speed, the traditional ways of testing distributed systems break down. You can’t just “inject a delay” using tc qdisc because the delay you’re looking for is 50 nanoseconds, not 50 milliseconds.

Why Recent Hype is Actually Substantiated

In the last couple of years, the engineering community has been buzzing about FoundationDB and TigerBeetle. These databases are famous not just for their performance, but for their radical commitment to simulation testing. They don’t just test their code; they build a “virtual world” where their code lives. For HFT, this isn’t just a “nice-to-have” anymore—it’s a prerequisite for survival.


The Philosophy of Determinism: Turning Time into an Index

The core problem with distributed systems is non-determinism. Every time you run your program, it behaves differently because:

  1. The System Clock: System.currentTimeMillis() is never the same twice.
  2. Thread Scheduling: The OS scheduler decides when to preempt your thread based on things totally unrelated to your code.
  3. The Network: Packets arrive in random orders, get dropped, or get duplicated.
  4. Disk I/O: Latency varies based on SSD wear-leveling or file system fragmentation.

Deterministic Simulation Testing solves this by making the entire environment a pure function. If you provide the same Seed, you get the same Output. Every single time.

The Anatomy of the Simulator

To achieve this, we had to re-architect our consensus engine from the ground up. We moved away from calling OS-level primitives directly. Instead, we injected a “Simulation Layer.”

1. The Virtual Clock

In the simulator, “Time” is just a counter. If a node wants to know the time, it asks the IClock interface. In production, this maps to CLOCK_REALTIME. In simulation, it maps to a value controlled by the scheduler. This allows us to speed up time, slow it down, or even stop it.

2. The Deterministic Scheduler

Instead of using std::thread or pthread, all tasks are submitted to a central scheduler. The scheduler maintains a priority queue of events.

  • “Node A sends Heartbeat to Node B at T=10ms”
  • “Node C experiences Disk Write Completion at T=12ms”

The simulator picks an event, executes it to completion, and moves to the next. Because the execution is single-threaded from the perspective of the simulation, we eliminate race conditions in the simulator itself.

3. The Fault-Injected Network

We replaced the socket layer with a virtual switch. This switch doesn’t just pass messages; it acts like a malicious deity. Based on the random seed, it might:

  • Drop a packet: “Oh, you wanted that AppendEntries request? Too bad.”
  • Reorder packets: Send the “Commit” message before the “Propose” message.
  • Partition the network: Split the cluster into two factions that can’t talk to each other.

Deep Dive: Implementing the Discrete Event Simulator

Let’s look at the actual engineering behind the scheduler. We utilize a Discrete Event Simulation (DES) model. In this model, the state of the system only changes at discrete points in time.

struct Event {
    uint64_t timestamp_ns;
    uint32_t node_id;
    std::function<void()> action;

    // Ordered by timestamp for the priority queue
    bool operator>(const Event& other) const {
        return timestamp_ns > other.timestamp_ns;
    }
};

class Simulator {
    std::priority_queue<Event, std::vector<Event>, std::greater<Event>> event_queue;
    uint64_t current_time_ns = 0;

public:
    void schedule(uint64_t delay_ns, uint32_t node_id, std::function<void()> action) {
        event_queue.push({current_time_ns + delay_ns, node_id, action});
    }

    void run() {
        while (!event_queue.empty()) {
            auto event = event_queue.top();
            event_queue.pop();

            // Jump the clock forward to the next event
            current_time_ns = event.timestamp_ns;

            // Execute the event
            event.action();
        }
    }
};

This looks simple, but the implications are profound. Because the current_time_ns only moves when an event finishes, you can simulate a 24-hour trading day in roughly 30 seconds of real-world time.

Handling the “External World”

The hardest part of DST is Hermeticity. Your code must not leak into the real world.

  • No Random Numbers: You cannot use rand() or std::mt19937 seeded with time(). You must use a PRNG (Pseudo-Random Number Generator) where the seed is part of the simulation state.
  • No Direct I/O: Every disk write must go through a virtualized file system that can simulate “Disk Full” errors or “Latent Sector” failures.
  • Memory Management: Even memory allocation can be a source of non-determinism if you rely on pointers as keys in a map (since the heap address might change). We enforce deterministic allocation or avoid using raw pointers in logic.

The “God Mode” Debugging Experience

When a test fails in the simulator, it produces a Seed. Example: Simulation failed with Seed: 0xDEADBEEF1234.

This is the holy grail of HFT engineering. An engineer can take that seed, run the simulator on their local laptop, and the code will fail at the exact same nanosecond, with the exact same stack trace. No more “I can’t reproduce this.” You can attach a debugger, step through the code, and see exactly why the Raft leader failed to step down during that specific network partition.

Property-Based Testing in Simulation

We don’t just write manual test cases. We use Property-Based Testing. We define “Invariants”—things that must always be true, no matter how much chaos the simulator injects.

  1. Election Safety: There is at most one leader per term.
  2. Log Matching: If two nodes have a log entry at the same index and term, the logs are identical up to that index.
  3. State Machine Safety: If a node has applied a log entry at a given index, no other node will ever apply a different entry at that same index.

The simulator runs millions of permutations—shuffling network packets, crashing nodes, corrupting disk sectors—while constantly checking these invariants.


Challenges: The Cost of Determinism

It sounds like magic, but implementing DST for an HFT engine is an uphill battle against the language and the hardware.

1. The SIMD and Compiler Trap

In HFT, we use a lot of AVX-512 and compiler-specific optimizations. Occasionally, different versions of a compiler (or different CPU architectures) might generate slightly different floating-point results due to instruction reordering. To maintain strict determinism, we have to be extremely careful with floating-point math, often opting for fixed-point arithmetic for the core consensus logic.

2. The Challenge of Third-Party Libraries

If your consensus engine uses a third-party library for logging or networking, and that library calls std::thread or gettimeofday(), your determinism is shattered. This forced us to write almost everything from scratch or heavily wrap third-party dependencies in deterministic interfaces.

3. Simulating RDMA and Kernel Bypass

In production, we use RDMA (Remote Direct Memory Access) via RoCE v2. Simulating the nuances of RDMA—like memory registration, completion queues, and zero-copy semantics—is non-trivial. We had to build a high-fidelity model of an RDMA NIC that correctly simulates the asynchronous nature of hardware work queues within our discrete event scheduler.


Real-World Impact: Finding the “Impossible” Bug

A few months ago, our simulator found a bug that would have been impossible to catch otherwise.

  • Scenario: A 5-node cluster.
  • Condition: Node 1 is the leader. Nodes 2 and 3 are followers.
  • Chaos: The simulator injected a network partition where Node 1 could talk to Node 2, and Node 2 could talk to Node 3, but Node 1 could not talk to Node 3 (A “Hidden Terminal” problem in the network topology).
  • Trigger: Simultaneously, the simulator injected a “Disk Slow” event on Node 2’s WAL (Write Ahead Log).

Because of the specific timing of the heartbeats and the slow disk write, Node 2 was able to acknowledge a log entry to the leader before it was actually persisted to its own local storage. If a power failure had occurred at that exact moment, the cluster would have lost a “committed” trade.

The simulator caught this in about 15 minutes of fuzzing. In production, this might have happened once every two years, but it would have cost millions.


Verification as a First-Class Citizen

Deterministic Simulation Testing changes the culture of an engineering team. It moves the conversation from “Did you test this?” to “Does the simulator prove this invariant holds?”

In the high-stakes world of High-Frequency Trading, where the difference between profit and a catastrophic loss is measured in microseconds and race conditions, DST is the ultimate competitive advantage. It allows us to innovate faster, knowing that our “God Mode” simulator is checking our work against the chaos of the real world.

If you are building distributed systems today—whether it’s an HFT engine, a database, or a cloud orchestrator—and you aren’t using deterministic simulation, you are essentially building on sand. It’s time to take control of time itself.


Engineering Checklist for Implementing DST:

  • Abstract the Clock: Replace all now() calls with an injectable IClock.
  • Modularize Networking: Create a LinkLayer interface that can be replaced by a VirtualSwitch.
  • Single-Thread the Logic: Ensure your core state machine is decoupled from threading primitives.
  • Seed Everything: Ensure every bit of randomness (backoffs, leader election timeouts) is derived from a single parent seed.
  • Invariant Checking: Write “Audit” functions that run after every discrete event to verify the system state.

The road to 100% reliability is long, but it starts with a single, deterministic step.


More to explore

Keep diving in