Deterministic lockstep is the backbone of many real-time multiplayer games, from RTS classics to modern fighting games. The core promise is simple: every peer simulates the same world, and only player inputs travel over the wire. But when peers run on different CPUs, GPUs, operating systems, or network conditions, that promise breaks. Floating-point rounding, timer drift, thread scheduling, and even the order of hash-map iterations can cause divergence. This guide is for engineers who already understand the basics of lockstep and need practical patterns for heterogeneous peer topologies—where no two peers are identical.
Why Heterogeneous Topologies Break Naive Lockstep
The textbook lockstep model assumes identical simulation environments. Every peer runs the same binary, the same floating-point math, the same deterministic random generator. In practice, that assumption rarely holds. A peer on an ARM-based MacBook will compute a different result for the same trigonometric function than a peer on an x86 Windows machine, even with the same source code. Compiler optimizations, library versions, and even the order of operations in a shader can introduce non-determinism.
Beyond hardware, network topology adds another layer of heterogeneity. Peers may have asymmetric latency, packet loss patterns, or NAT traversal constraints. A peer behind a strict NAT might not be able to send direct messages to all other peers, forcing a relay topology. The lockstep protocol must handle these variations without introducing simulation divergence.
What usually breaks first is the assumption that all peers will process the same inputs in the same order. In a star topology with a relay server, the relay serializes inputs, but the relay itself becomes a single point of failure and a potential source of non-determinism if it reorders messages. In a peer-to-peer mesh, each peer must agree on the global order of inputs without a central arbiter. This is where clock synchronization and consensus algorithms come into play.
Teams often find that the first sign of trouble is a desync detection that fires after a few seconds of gameplay. The checksum mismatch points to a subtle difference in simulation state. The root cause is rarely a single bug—it's a combination of platform quirks, network timing, and algorithmic assumptions that worked in the lab but fail in the wild.
Common Sources of Non-Determinism
Floating-point determinism is the most discussed, but not the only source. Thread scheduling can cause race conditions in lockstep simulation if any peer uses parallel processing without careful synchronization. Even a simple std::unordered_map iteration can produce different orders across compilers. Input handling—such as the exact timestamp of a keypress—can vary if peers use different clock sources. The solution is to establish a contract: every peer must use the same arithmetic, the same data structures, and the same input processing logic. That contract is enforced through a deterministic virtual machine or a set of coding guidelines verified by automated tests.
Core Mechanism: Input Authority and State Checksums
Deterministic lockstep across heterogeneous peers relies on two pillars: input authority and state checksums. Input authority means that every peer agrees on which inputs are valid and in what order they are applied. In a typical implementation, each peer collects its own inputs for a fixed time step (e.g., 100 ms) and broadcasts them to all other peers. After the broadcast window closes, every peer has the same set of inputs for that step. The simulation advances by applying those inputs in a deterministic order—usually sorted by peer ID and timestamp.
State checksums provide the safety net. At the end of each simulation step (or every N steps), each peer computes a cryptographic hash of the entire game state. Peers exchange these hashes. If any peer detects a mismatch, it triggers a desync recovery: it pauses the simulation, requests a full state snapshot from a trusted peer, and reconciles. The checksum must be computed on a deterministic subset of the state—only the data that affects gameplay. Visual-only state (like particle effects) should be excluded to avoid false positives.
The catch is that checksums add latency and bandwidth. For a 60-tick simulation, computing a hash every tick can be expensive. A common optimization is to compute checksums only every 10 ticks, or to use a rolling hash that updates incrementally. Another pattern is to use a Merkle tree of state regions, so that only the changed regions need to be rehashed. This reduces the cost while still catching desyncs quickly.
Input authority itself requires a consensus on the global input order. In a peer-to-peer mesh, peers use a distributed clock synchronization protocol (like NTP-derived offsets) to assign timestamps. But clock drift between peers can cause disagreement about which inputs belong to which step. A robust approach is to use a logical clock: each peer increments a step counter, and inputs are tagged with that step number. The peer that generates the input also includes its local step counter. Other peers map that step counter to their own step counter using a known offset. If the offset is uncertain, the input is buffered until the mapping is resolved.
Handling Late or Missing Inputs
In heterogeneous networks, inputs can arrive late or not at all. The lockstep protocol must define a timeout. If an input does not arrive within the expected window, the peer can either pause and wait (introducing lag) or simulate with a default input (like a no-op). The latter risks desync if other peers choose a different default. A safer pattern is to use a deterministic null input that all peers agree on—for example, the previous frame's input repeated, or a neutral action. But this only works for short durations. For longer outages, the session should pause or the missing peer should be dropped.
How It Works Under the Hood: A Layered Architecture
Building a deterministic lockstep system for heterogeneous peers requires a layered architecture. At the bottom is the transport layer, which handles message delivery. Above that is the synchronization layer, which manages input ordering and clock alignment. Above that is the simulation layer, which runs the deterministic game logic. Each layer must be designed to tolerate heterogeneity.
The transport layer should use a reliable, ordered protocol (like TCP or a reliable UDP library) for input messages. But reliability alone is not enough—the transport must also handle NAT traversal and peer discovery. In a heterogeneous topology, some peers may be behind symmetric NATs that prevent direct connections. A relay server can forward messages, but the relay introduces latency and a single point of failure. An alternative is to use a hybrid mesh: peers that can connect directly do so, while others use the relay. The relay must be stateless and forward messages without reordering.
The synchronization layer is where most of the complexity lives. It must ensure that all peers agree on the current step number and the set of inputs for that step. A common pattern is to use a two-phase commit: in the first phase, each peer broadcasts its inputs for the next step. In the second phase, after all inputs are collected, each peer broadcasts a commit message that includes the hash of the collected inputs. If a peer receives a commit with a different hash, it knows a desync has occurred. This pattern is similar to the classic lockstep but with added fault tolerance for late messages.
Clock synchronization is handled by a separate sub-layer. Peers exchange timestamps and compute round-trip times to estimate offset. The offset is used to convert local timestamps to a common time base. However, clock drift is inevitable. The synchronization layer must periodically re-sync clocks and adjust the step timing. A drift of even a few milliseconds per minute can cause step boundaries to slip, leading to input misalignment. One solution is to use a fixed step duration (e.g., 100 ms) and allow peers to run slightly ahead or behind, buffering inputs to compensate. The buffer depth must be large enough to absorb jitter but small enough to keep latency acceptable.
Deterministic Simulation Engine
The simulation layer must be a deterministic virtual machine. All floating-point operations should use a fixed-precision library or be replaced with integer arithmetic. Random number generators must use a seeded, deterministic algorithm (like xorshift) with the same seed on all peers. The simulation must be single-threaded or use a deterministic thread scheduler. Any use of external libraries (like physics engines) must be verified for determinism across platforms. A practical approach is to run the simulation in a sandboxed environment, like a WebAssembly module, which guarantees the same behavior regardless of the host platform.
Worked Example: Real-Time Strategy Game with Mixed Peer Topology
Consider a real-time strategy game with up to 8 players. The game uses a peer-to-peer mesh, but some players are on mobile devices with variable network quality, while others are on desktop with stable connections. The lockstep protocol must handle this heterogeneity without giving an advantage to any peer.
Step 1: Session setup. Each peer connects to a matchmaking server that provides the list of peer addresses. The server also assigns a session ID and a deterministic seed for the random number generator. Each peer computes its own clock offset relative to the server's clock. The server acts as a time authority, but it does not participate in the simulation.
Step 2: Input collection. Each peer collects player inputs for a fixed step of 100 ms. The inputs are packaged with the peer's ID and the step number (based on the server's clock). The peer broadcasts the input message to all other peers via the transport layer. If a peer cannot reach another directly, the message is relayed through the server.
Step 3: Input commitment. After 150 ms (allowing for network jitter), each peer has a set of received inputs for the current step. It computes a hash of the sorted inputs and broadcasts a commit message containing that hash. If a peer receives a commit with a different hash, it knows a desync has occurred. It then requests a full state snapshot from the server (which is authoritative) and reconciles.
Step 4: Simulation. Each peer applies the inputs in deterministic order (sorted by peer ID, then by timestamp). The simulation advances by one step. At the end of the step, each peer computes a checksum of the game state (excluding visual-only data) and broadcasts it. If all checksums match, the step is confirmed. If not, the peer with the mismatched checksum requests a state snapshot and reconciles.
In this example, the mobile peer might have higher latency and packet loss. The protocol handles this by allowing the mobile peer to send its inputs earlier (based on its clock offset) so that they arrive before the commit deadline. If the mobile peer's input is lost, other peers simulate with a null input for that step. The mobile peer detects the loss when it receives a commit hash that does not match its own input set. It then resends its input and requests a state snapshot to catch up.
Handling Peer Dropouts
If a peer disconnects permanently, the remaining peers must continue the simulation without it. The protocol should detect the dropout after a timeout (e.g., 5 seconds). The remaining peers agree to remove the dropped peer from the input authority set. The simulation continues with the remaining peers, but the game state may need to be adjusted—for example, the dropped player's units become neutral or are removed. The decision must be deterministic: all peers must apply the same rule. A common pattern is to have a deterministic function that removes the dropped player's entities from the state.
Edge Cases and Exceptions
Even with careful design, edge cases arise. One is the case of a peer that rejoins after a temporary disconnection. The rejoining peer must receive a full state snapshot from a trusted peer and then replay the inputs it missed. But the snapshot must be from a step that all peers agree is consistent. The rejoining peer must also re-synchronize its clock. This process can take several seconds, during which the rejoining peer is not participating.
Another edge case is the presence of a malicious peer that intentionally sends incorrect inputs or commit hashes. In an untrusted environment, the protocol must include a verification mechanism. One approach is to use a commit-reveal scheme: peers first commit to a hash of their inputs, then reveal the inputs. Other peers can verify that the revealed inputs match the hash. If a peer cheats, it is detected and expelled. However, this adds latency and complexity. For many games, the trust model is that all peers are running the same client software and are not malicious, so this is not needed.
Clock skew is a persistent issue. Even with NTP, clock drift can accumulate. Over a 30-minute game, a drift of 10 ms per minute results in a 300 ms offset. This can cause step boundaries to slip, leading to input misalignment. A solution is to use a dynamic step duration: if a peer's clock is drifting, the step duration is adjusted slightly to realign. For example, if a peer is running 10 ms behind, the next step is shortened by 10 ms. This keeps the step boundaries aligned without requiring a global clock reset.
Finally, there is the edge case of floating-point determinism across different hardware. Even with fixed-precision arithmetic, some operations (like division by zero) can produce different results on different platforms. The simulation must define behavior for all edge cases: for example, division by zero results in a maximum value. All peers must use the same definition.
Limits of the Approach
Deterministic lockstep is not a silver bullet. It has fundamental limits that become more pronounced in heterogeneous topologies. The first limit is scalability. As the number of peers grows, the bandwidth for broadcasting inputs grows quadratically in a full mesh. For 8 peers, each peer sends 7 input messages per step. For 64 peers, that becomes 63 messages per step, which can saturate uplinks. A relay server can reduce this to one message per peer, but the relay becomes a bottleneck.
The second limit is latency. Lockstep requires waiting for the slowest peer's input before advancing. In a heterogeneous network, the slowest peer (e.g., a mobile device on 3G) can drag down the experience for everyone. The protocol can use predictive inputs (where peers simulate ahead and correct later), but this introduces complexity and potential desyncs.
The third limit is determinism itself. Some game features are inherently non-deterministic, such as physics simulations with chaotic behavior or AI that uses machine learning models. These features cannot be used in a lockstep model unless they are replaced with deterministic approximations. The trade-off is between simulation fidelity and determinism.
Finally, the approach assumes that all peers are running the same simulation binary. If some peers have a different version of the game (e.g., due to updates), the simulation will diverge. The protocol must enforce version matching at session start. This is a practical limitation for live service games where clients update asynchronously.
Reader FAQ
How do I handle peer-to-peer NAT traversal in a lockstep system?
Use a relay server for peers that cannot establish direct connections. The relay should be stateless and forward messages without modification. For best performance, use a hybrid approach: peers that can connect directly do so, while others use the relay. The relay must be on a low-latency path to minimize added delay.
What is the best way to detect desyncs early?
Use incremental checksums computed every few steps. A Merkle tree of state regions allows you to pinpoint the exact region that diverged. Also, log all inputs and simulation steps on each peer so that you can replay the session to find the root cause.
Can I use deterministic lockstep with more than 8 players?
Yes, but you need to switch from a full mesh to a relay or server-authoritative model. The bandwidth and latency constraints make full mesh impractical beyond 8–16 players. A server-authoritative model where the server runs the simulation and clients send inputs is more scalable, but it is not true lockstep—it is a client-server model with deterministic simulation on the server.
How do I test determinism across heterogeneous platforms?
Set up a continuous integration pipeline that runs the simulation on multiple platforms (Windows, macOS, Linux, ARM) with the same inputs and compares the output checksums. Use a deterministic random seed and fixed time steps. Automate the detection of any checksum mismatch.
What should I do if a peer's clock drifts too much?
If the drift exceeds a threshold (e.g., 500 ms), the peer should be considered out of sync and forced to rejoin. The protocol can detect drift by comparing the peer's step number with the expected step number based on elapsed real time. If the difference is too large, the peer is dropped.
Is it worth using a deterministic virtual machine like WebAssembly?
Yes, if you have the resources. WebAssembly guarantees deterministic execution across platforms, eliminating many sources of non-determinism. The trade-off is performance overhead and the complexity of porting your simulation to Wasm. For new projects, it is a strong investment. For existing codebases, it may be more practical to enforce deterministic coding guidelines and test rigorously.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!