10 min read

The God Mode of Engineering: Hunting Heisenbugs with Deterministic Simulation Testing in Multi-Paxos

Deterministic Simulation Testing for Multi-Paxos Heisenbugs

The God Mode of Engineering: Hunting Heisenbugs with Deterministic Simulation Testing in Multi-Paxos

It’s 3:15 AM on a Tuesday. Your pager goes off. A massive-scale Multi-Paxos cluster—the backbone of your company’s global metadata store—just lost quorum in a way that “mathematically shouldn’t happen.” You look at the logs. There is a gap in the sequence numbers that suggests a leader was elected while another leader still thought it held the lease, but only for a window of 400 microseconds, and only because a disk write latency spike coincided with a specific network partition and a garbage collection pause.

You try to reproduce it in the staging environment. You can’t. You try to reproduce it locally. You can’t. You are chasing a Heisenbug: a bug that disappears or changes its behavior when you attempt to observe or probe it.

In the world of distributed systems, traditional testing is a lie. Unit tests are too narrow. Integration tests are too flaky. Even Jepsen—as legendary as it is—is often too slow to find the one-in-a-billion race condition before your customers do.

If you want to build a system that survives the chaos of the real world at a massive scale, you need something better. You need Deterministic Simulation Testing (DST). You need the ability to shrink a 1,000-node cluster into a single-threaded process, control time, manipulate the network, and—most importantly—reproduce any failure with 100% bit-for-bit accuracy using nothing but a random seed.

This is how we built the “God Mode” for our Multi-Paxos implementation.


The Distributed Consensus Paradox

Distributed consensus is the art of getting a group of unreliable computers to agree on a single value, even when the world is burning around them. Multi-Paxos is the gold standard for this, allowing for a continuous stream of agreed-upon values (a log).

The paradox is that while the Paxos algorithm itself is “proven” correct, the implementation of Paxos at scale is an absolute minefield. When you move from a white paper to a production system handling millions of requests per second across global data centers, you introduce:

  1. Optimizations: Multi-leader optimizations, pipelining, batching, and master leases.
  2. Environmental Gremlins: Clock skew, partial disk failures, silent data corruption, and “gray” network failures.
  3. Concurrency: Lock contention, thread scheduling, and asynchronous I/O.

In a standard environment, these variables are non-deterministic. The OS kernel decides when a thread wakes up. The NIC decides when a packet arrives. The hardware decides when a bit flips. If a bug occurs, the state of the universe that caused it is lost forever.


Enter Deterministic Simulation Testing (DST)

DST is a philosophy popularized by systems like FoundationDB and recently highlighted by TigerBeetle DB. The core idea is simple but transformative: The entire world is a function of a single seed.

Instead of running your Paxos nodes on a real OS with real networking, you run them inside a Discrete Event Simulator.

The Architecture of the Simulator

In our architecture, the “World” is a single-threaded loop that manages a priority queue of events. These events could be:

  • A packet arriving at a node.
  • A timer firing.
  • A disk I/O operation completing.
  • A new client request entering the system.
while (!event_queue.empty()) {
    Event e = event_queue.pop();
    world.time = e.timestamp; // Virtual time jumps!
    dispatch_event(e);
}

Because the simulator is single-threaded and the order of events is dictated by a pseudo-random number generator (PRNG) initialized with a seed, the entire execution is deterministic. If you run the simulation with Seed 0xDEADBEEF, you will get the exact same interleaving of packets and disk writes every single time.


Eliminating the “Pollutants” of Non-Determinism

To make this work, we had to perform a “surgical extraction” of every non-deterministic source in our C++ codebase. This is the hardest part of implementing DST, and it requires extreme discipline.

1. Virtualizing Time

You cannot call gettimeofday() or std::chrono::system_clock::now(). In a simulation, time doesn’t move unless the simulator says it does. We abstracted time into a Clock interface. In production, it calls the OS; in simulation, it returns the current “Virtual Time” of the event loop. This allows us to simulate “a week in the life” of a cluster in just a few seconds of real-world CPU time.

2. The Deterministic Scheduler

You cannot use std::thread. Period. Threads are scheduled by the OS kernel, which is the definition of non-determinism. Instead, we wrote our Paxos nodes to be State Machines that respond to events. If a node needs to do “background work,” it schedules a future event with the simulator.

3. Virtualizing I/O (The Network and Disk)

We created an abstraction layer for all syscalls.

  • The Network Simulator: Can drop packets, reorder them, duplicate them, or delay them based on a “Chaos Model.”
  • The Disk Simulator: Can simulate “Latent Sector Errors,” partial writes (torn pages), and fluctuating IOPS.

4. The “No-No” List

To maintain bit-for-bit determinism, we had to ban:

  • Address-as-a-key: You cannot use a pointer address as a key in a map, because ASLR (Address Space Layout Randomization) will change those addresses across runs.
  • Uninitialized Memory: A single uninitialized bool can branch your simulation into a parallel universe. We use Valgrind and MSAN religiously.
  • Non-deterministic Hash Maps: Iterating over a std::unordered_map can vary between compilers or even runs. We switched to ordered maps or deterministic hash seeds for simulation.

Scaling the Simulation: 10,000 Nodes in a Single Process

The “Massive-Scale” part of our title isn’t hyperbole. In a real-world cluster, you might have 5 or 7 nodes in a Paxos group, but you might have thousands of such groups (Multi-Paxos) spread across a fleet.

By using a single-threaded simulator, we eliminated the overhead of context switching and locking. We can simulate 1,000 nodes on a single MacBook Pro because those nodes are just objects in memory. They aren’t expensive OS processes; they are state machines waiting for the next PacketReceived event.

The Adversary: Intelligent Chaos

A random walk through the state space is fine, but it’s inefficient. To truly test Multi-Paxos, we built an Adversary.

The Adversary is a component in the simulator that has “God’s Eye View” of the entire cluster state. It doesn’t just drop random packets; it looks at the Paxos state and asks:

  • “Who is the current Proposer?”
  • “Who is in the Quorum?”
  • “What is the most fragile moment to drop a PrepareResponse?”

The Adversary then targets those specific packets. This is Property-Based Testing on steroids. We aren’t just looking for crashes; we are looking for Safety Violations.


The Technical Substance: Finding the “Double-Elected” Bug

Let’s look at a real bug the DST found.

In Multi-Paxos, you use a Leader Lease to ensure that only one leader is active at a time, allowing for “Local Reads” (reading from the leader without a full round of consensus).

The logic was:

  1. Leader A wins a Paxos round.
  2. Leader A sets lease_expiry = now() + 5s.
  3. Leader A services reads locally until now() > lease_expiry.

During a simulation run with Seed 42, the Adversary triggered a “Network Partition” exactly as Leader A was renewing its lease. However, the Adversary also simulated a Disk Stall on Leader A’s local metadata log.

Because the code was using a monotonic clock for the lease but a wall clock for the log-append timestamp, a logic error occurred where the lease was considered “active” even though the underlying consensus state had moved to a new Epoch.

Without DST: This bug would have happened once every six months in production. The logs would have been inconclusive. With DST: We got a 2MB log file that reproduced the exact sequence. We fixed the code, re-ran Seed 42, and verified the fix in 10 seconds.


Infrastructure: The Simulation Farm

Scaling the testing is just as important as scaling the system. We built a “Simulation Farm” that runs millions of unique seeds 24/7.

  • Compute Scale: We utilize a spot-instance cluster of 500 nodes (64 cores each).
  • Throughput: We run roughly 100 million simulated “events” per second across the farm.
  • The “Golden Seed”: Every time a bug is found, that seed is added to a “Regression Suite.” This suite is run on every Pull Request.

Code Snippet: The Node Interface

This is a simplified look at how a Paxos Node interacts with our deterministic world:

class MultiPaxosNode {
public:
    // Every input comes through a deterministic dispatcher
    void on_receive(Envelope packet) {
        auto now = world->clock().now();

        if (packet.message.type() == PREPARE) {
            handle_prepare(packet.message.as_prepare(), now);
        }
        // ... other handlers
    }

    void handle_prepare(const Prepare& req, TimePoint now) {
        if (req.ballot > promised_ballot) {
            promised_ballot = req.ballot;
            // Instead of sending a real packet, we tell the simulator to queue it
            world->network().send(packet.source, Promise{promised_ballot, accepted_value});

            // Persist to "Disk" (the simulator's virtual disk)
            world->disk().append(LogEntry{promised_ballot});
        }
    }

private:
    Ballot promised_ballot;
    // ... other state
};

Why the Hype Around DST is Real

Lately, there’s been significant industry buzz around “Formal Methods” (like TLA+) and “Simulation Testing.” You’ve likely seen the white papers from Amazon (S3) or MongoDB discussing their use of these techniques.

The hype exists because we have reached the limits of Observation-Based Engineering. We can no longer “observe” our way to reliability in systems with $N^2$ complexity.

TLA+ (Formal Methods) is great for verifying the algorithm, but it doesn’t find bugs in your C++ memcpy logic or your handling of 64-bit integer overflows.

DST (Deterministic Simulation) bridges the gap between the mathematical proof and the messy reality of code. It gives you the confidence that your implementation actually follows the proof. It turns “I think this works” into “I have simulated 10 billion state transitions and it hasn’t broken yet.”


Engineering Curiosities: The “Ghost in the Machine”

One of the most fascinating aspects of building this was discovering how many things we take for granted are actually non-deterministic.

Did you know that in some environments, the order of environment variables in your shell can change the memory layout of your stack, which changes the addresses of objects, which—if you’re using a hash map that isn’t seed-stabilized—can change the order of your Paxos log processing?

We found that we had to wrap everything. Even rand() is a global state. We replaced it with a per-node PRNG.

We even found a bug in the compiler’s optimization of a specific loop that only triggered when the simulation ran on an ARM64 architecture versus x86_64. Because the simulator itself was deterministic, we could compare the execution traces of the two architectures and find the exact instruction where they diverged.


The “Holy Grail” of Debugging: Time Travel

Because the simulation is deterministic and based on a seed, we implemented Time Travel Debugging.

If a simulation fails at T = 1000ms, we can:

  1. Reload the seed.
  2. Run the simulation to T = 999ms.
  3. Turn on DEBUG level logging.
  4. Step through the code line-by-line in GDB.

This effectively turns the most complex distributed systems problems into local, single-threaded debugging sessions. No more adding printf statements and hoping the bug happens again. You are the master of time.


Building for the Next Decade

As we move toward even more massive scales—think global clusters spanning dozens of regions and millions of nodes—the old ways of testing will continue to fail. You cannot build a “Five Nines” (99.999% availability) system on a foundation of “probably.”

Implementing Deterministic Simulation Testing for our Multi-Paxos clusters was a massive upfront investment. It took months to abstract the I/O, months to harden the scheduler, and months to build the adversary models.

But the payoff is a system that we know works. When we ship a new optimization to our Paxos engine, we don’t hold our breath. We run 10 million simulations. If the seed is green, we merge.

In the high-stakes world of distributed consensus, DST isn’t just a testing strategy. It’s the only way to sleep through the night.

If you’re building a distributed system today, ask yourself: Can you reproduce a production failure with a single seed? If the answer is no, you aren’t in control of your system—the chaos is.


More to explore

Keep diving in