Back to Devlog
    Deep dive
    EngineSeptember 22, 2026

    The Netcode, As It Actually Is

    125 Hz authoritative simulation, a 100 ms rewind budget, 62.5 snapshots a second, and no rollback anywhere. What shipped between v0.9.0 and v0.9.23, with the numbers from source.

    Not Rollback, And Never Was

    Worth stating first, because it is the usual claim for an arena shooter and it is not what we built. There is no rollback implementation in src/. The design notes say so outright — "Replay from server state (NOT rollback - just re-predict)" — and the client agrees: when it cannot reconcile, it adopts the server's state outright and discards the commands that can no longer be honoured.

    There is also no SharedArrayBuffer. Zero occurrences in src/. It appears seven times across two aspirational research documents, which is not the same thing as shipping it.

    What we run is a Quake-lineage authoritative server. The server owns the simulation, the client predicts its own movement, and disagreement is resolved in the server's favour.

    The Numbers

    From source, with the version that set each one:

    Simulation tick125 Hz (8 ms)v0.9.0
    Snapshot cadencerequests 60/s, broadcasts every 2nd tick = 62.5 Hzv0.9.3
    Lag compensation100 ms rewind budgetwired v0.9.23
    Interpolation50 ms default, auto-tuned, clamped 16-150 msv0.9.3
    Extrapolation cap60 ms on packet loss, down from 200v0.9.23
    Delta history64 server-side, 128 client-sidev0.9.3 / v0.9.23

    The snapshot rate deserves a note, because the changelog and the source disagreed about it for a while. The release notes call it "60Hz". The code requests 60, divides 125 by it, and lands on 62.5 — it broadcasts every second tick. v0.9.18 finally admitted it: a nominal 1x recording was running ~4.17% slow.

    One Rewind Budget, On Purpose

    Hit registration rewinds the world to the view the shooter actually had, keyed off the view tick the client reports rather than its ping. The budget is a single global 100 ms.

    That is a fairness decision, and it is deliberately not configurable per match. The reasoning is in lagcomp/config.ts: no setting is fair to both ends of every connection, so there is no setting.

    Worth being precise about the history here, because it was a real bug. LagCompensation shipped as a component back in v0.9.0 — and GameServer never instantiated it. It was only ever wired up by the CLI. v0.9.23 fixed that, and in doing so found that hitscan was resolving against the present on the live server with no rewind of any kind.

    Prediction, Replay, And Their Limits

    Client and server run the same MovementProfile, so the rules cannot drift between them. When a snapshot arrives, the client resets to the authoritative position and velocity and re-predicts the inputs the server has not yet acknowledged.

    Up to 32 unacknowledged inputs. Past that it stops trying and resynchronises instead. This post previously claimed it replayed the full input history, and cited a 128-entry replay budget. Both wrong: 128 is the client's delta-decode buffer, a different thing entirely. The replay cap is 32 and the stored input ring is 256.

    Bandwidth Is A Visibility Problem

    The server does per-client PVS cluster filtering when building snapshots, so you receive entities you could plausibly see. It arrived in v0.9.5, along with per-client delta baselines, and SnapshotBroadcaster was split out of GameServer in v0.9.9 so the encoding had a home of its own.

    Cheating and Diagnostics

    • Input is sanitised at ingest — InputSanitizer became the single choke

    point in v0.9.16, called by both the server and client prediction, so the predicted and simulated states apply an identical transform.

    • Anti-warp validation — and here the previous version of this post was wrong

    in the other direction. It said the whole thing was log-only. It is not. The rate cap still drops flooding inputs outright. What became advisory in v0.9.16 was the real-time budget, which records strikes instead of discarding the command.

    • Clock sync — 5-ping burst on connect, resync every 30 s, highest-RTT

    outliers discarded and the rest averaged. v0.9.2.

    • Command redundancy — each tick the client re-sends its last three encoded

    commands as three separate unreliable datagrams, and the server deduplicates by sequence number. The old version of this post called them "the last 3 commands per packet"; the count was right, the unit was not.

    • Server pacing — exactly one input consumed per tick, FIFO, and the last

    input repeated when none arrives so gravity and momentum still advance. v0.9.16.

    • `cl_netgraph 0|1|2` — off, text, or text and graph. Eight readouts:

    ping, snapshot rate, packet loss, interp delay, command rate, reconciliation rate, average ping, clock offset. v0.9.3 shipped seven; the average line came later.

    The client also refuses to connect to a server that is not 125 Hz. If the handshake disagrees about tick rate we would rather drop the connection than silently simulate at the wrong cadence.

    Bots Are Seats, Not Players

    Bots occupy lobby seats — a lobby of one human and any number of bots is a normal arrangement — but they are explicitly flagged: they carry no account, they authenticate with a shared secret, and they are excluded from ranked results. Their default brain is StateMachineBrain, upgraded to HybridBrain when trained weights are present.

    An earlier version of this post called them "real players" going through the same netcode as everyone else. They do go through the same netcode. They are not real players, and the lobby is careful about the difference.

    What Changed Here

    The body of this post used to be filed under v0.4. Nothing netcode shipped in v0.4 — that release was mobile performance, extracting inline styles into UIStyles.ts, and Electron desktop builds.

    The real span is v0.9.0 to v0.9.23: the fixed timestep and prediction model in v0.9.0, SnapshotBuffer and clock sync in v0.9.2, snapshot cadence and PVS filtering across v0.9.3 to v0.9.5, the ingest choke point and server pacing in v0.9.16, and the rewind wiring in v0.9.23. The shared movement profile itself predates all of it, in v0.8.23.