Lockstep vs rollback netcode

Deterministic lockstep and rollback are usually presented as rivals. They aren't, really — rollback is lockstep with one extra move bolted on. Both send inputs instead of world state, both demand that every client's simulation produce bit-identical results, both fall apart the same way when it doesn't. The only question that actually separates them is what your game does during the milliseconds it is waiting for the other player's input. This is a practical guide to choosing between them, and to the determinism work that decides whether either one survives contact with real players.

The shared foundation: send inputs, not state

In an input-synchronised model the network carries a few bytes per player per tick — a bitfield of buttons, maybe an analogue axis. Every client runs the identical simulation on those inputs and arrives at the identical world. Bandwidth is essentially independent of how much is happening on screen, which is why an RTS with 800 units can run over a modem-era connection while a state-replicating shooter with 20 players needs constant delta compression.

The price is absolute: the simulation must be deterministic. Not "close", not "converges" — the same inputs must produce the same bits on every machine, forever. One divergent bit at tick 400 doesn't cause a small glitch; it causes two players to be playing different games, and the difference compounds until someone dies on one screen and not the other. Everything hard about both models flows from that one requirement.

Lockstep: wait for everyone

Classic deterministic lockstep advances tick N only when the inputs of every player for tick N have arrived. Because it never guesses, it is never wrong — there is no correction, no snapping, no re-simulation. The implementation is small enough to hold in your head, which matters more than people admit.

The cost is input delay. If your opponent is 80 ms away, your own button press cannot be simulated until roughly a round trip has passed, so the game reacts around 80–160 ms after you press. Two mitigations are standard: buffer inputs a fixed number of ticks ahead (schedule input read at tick N for execution at tick N+3), and hide the remainder in animation — unit acknowledgement barks, wind-up frames, a click sound the instant you press. RTS games have been laundering latency through animation for thirty years, and it works.

Lockstep's other weak spot is that one lagging player stalls everyone. Real implementations add a timeout that either drops the straggler or lets the session "slow down" briefly, and that policy needs to exist before launch, not after the first bad connection.

Rollback: guess, then fix

Rollback refuses to wait. At tick N it simulates immediately, using a prediction of remote inputs — almost always "whatever they pressed last frame", which is right most of the time because humans hold buttons for many frames. When the real inputs for tick N arrive at tick N+4, the client compares them with what it predicted. If they match, nothing happens at all. If they don't, it restores the saved state from tick N, applies the true inputs, and re-simulates the four intervening frames before rendering the current one.

That gives local input a zero-latency feel: your character responds on the frame you pressed, because your own inputs are never predicted. The trade is consistency. A mispredicted remote input means the world visibly corrects. Prediction horizons beyond roughly 100–150 ms tend to feel bad, which is why practical rollback systems cap the prediction window (GGPO stops advancing when it hits its limit rather than guessing further ahead) and often reintroduce one or two frames of deliberate input delay — enough to cut misprediction rate substantially at a cost most players cannot perceive.

LOCKSTEP tick 40 tick 41 waiting for input… tick 42 always correct, always late ROLLBACK tick 40 state saved 41 pred. 42 pred. 43 pred. true input for 41 arrives restore tick 40, re-simulate 41–43 in one frame instant input, occasional correction
Same input-based foundation. The difference is whether you wait or guess.

The three prerequisites rollback adds

Everything lockstep demands, rollback demands too — plus:

  1. Save and restore of the entire simulation state, cheaply. You will do it many times a second. If a "save" means allocating objects and deep-cloning a scene graph, rollback is dead on arrival. State wants to live in flat typed arrays you can copy in one set() call.
  2. Simulation decoupled from rendering. You must be able to advance a tick without drawing it. If update and draw are tangled, re-simulating four frames means drawing four frames.
  3. CPU headroom. A worst-case frame is one normal tick plus N re-simulated ticks. If a tick costs 4 ms and your rollback window is 7 frames, your bad frame is ~32 ms — over budget at 60 FPS. Measure the re-simulation cost, not the average.

Those three constraints, not the network code, are what actually decide the answer for most projects.

Determinism in JavaScript and WebAssembly specifically

The classic advice — "floating point isn't deterministic across platforms" — comes from native C++, where compiler flags, x87 vs SSE and fused multiply-add all change results. On the web the picture is better than its reputation, and it pays to know exactly where the line is.

  • Basic arithmetic is safe. ECMAScript numbers are IEEE-754 doubles, and + - * / and Math.sqrt are exactly specified with round-to-nearest semantics. Every engine produces identical bits.
  • The Math library is not. The spec only recommends fdlibm-quality implementations for sin, cos, tan, exp, pow, log and friends — accuracy is implementation-approximated. Engines have changed these implementations over time. Assume any transcendental is a desync source and replace it with your own table, polynomial or fixed-point routine that ships with the game.
  • Wasm is deterministic with two footnotes. The core spec pins float behaviour, but NaN bit patterns are explicitly non-deterministic, and relaxed SIMD instructions are allowed to differ between engines. Don't hash raw state that can contain NaN, and keep relaxed SIMD out of simulation code.
  • Iteration order and randomness. Seed your own PRNG (and re-seed deterministically per tick if any subsystem consumes it); never iterate a collection whose order depends on insertion timing or object identity; never let Date.now(), frame time or actual delta-time leak into gameplay logic. Fixed tick rate, always.
  • Third-party physics. Engines legitimately randomise constraint solve order for stability; that alone will desync you. Either fix the seed per tick or use a physics library with a documented determinism mode — and re-test after every upgrade.

The pragmatic escape hatch is fixed-point maths: represent positions and velocities as integers scaled by, say, 216, and you sidestep the entire class of problem. It costs precision and developer comfort, and for cross-platform titles it is still the most common answer.

Build the desync detector first

Whichever model you choose, the single highest-value piece of infrastructure is a per-tick state checksum. Hash the simulation state at the end of every tick (or every eighth tick), exchange the hashes, and halt loudly with the tick number when they diverge. Without it, "the games drifted apart somewhere in the last four minutes" is a week of work. With it, you get "tick 1,842" and a replay you can bisect. Store the input stream too — an input log plus a starting state is a replay file, for free, which is one of the nicer side effects of input-based netcode.

So which one?

A rough decision rule that holds up in practice:

  • Rollback when the state is small, the player count is two (or a handful), and precise timing is the game: fighting games, platform fighters, small arena shooters, rhythm-adjacent PvP.
  • Lockstep when the state is large, the player count is higher, or actions have natural wind-up: RTS, simulation, tactics, most co-op-with-many-entities designs. Buy back responsiveness with animation and local UI feedback instead of prediction.
  • Neither when you have an authoritative server and untrusted clients. Input-based determinism assumes peers you can trust to run the real simulation. For anything where cheating or money is involved, use server-authoritative state replication with client-side prediction and reconciliation — the same prediction idea, but with a server as the single source of truth.

That last bullet is the one people skip. Lockstep and rollback are peer-to-peer trust models wearing a networking hat; they are excellent at bandwidth and terrible at adversaries. Pick the model that matches your threat model first, and the latency question second.

Related
→ Client-side prediction and rollback netcode in the browser → WebSocket vs SSE vs WebTransport → Object pooling: killing GC stutter at 60 FPS → Portfolio & projects
What is the difference between lockstep and rollback netcode?

Both send only inputs and require a deterministic simulation. Lockstep waits for every player's input before simulating a tick, so it is always correct but adds round-trip input delay. Rollback simulates immediately on predicted remote inputs, then restores a saved state and re-simulates when a prediction turns out wrong — near-zero input delay in exchange for occasional visible corrections.

Is rollback netcode always better than lockstep?

No. Rollback needs cheap save/restore of the whole state and CPU headroom to re-simulate several ticks inside one frame. Games with large state and many units — RTS, sims, tactics — normally stay on lockstep and hide latency in animation. Rollback shines in small-state twitch games.

What causes netcode desyncs?

Unseeded randomness, unordered iteration, wall-clock or delta-time dependent logic, platform floating-point differences, physics engines randomising solver order, and gameplay code reading raw input instead of the synchronised input buffer. Exchange a per-tick state checksum so you learn which tick diverged.

Is JavaScript floating-point maths deterministic across browsers?

The operators are: JS numbers are IEEE-754 doubles and + - * / and Math.sqrt are exactly specified. Math.sin, cos, pow, exp and the other transcendentals are only implementation-approximated and can differ between engines — replace them in simulation code.