Multiplayer: sources and measurements

The research behind the multiplayer skill, the C# example's Net/ model, the soak command and the enet-server-channels rule, gathered on 2026-10-21. Tests ran on Godot 4.7.2 (mono and standard) and .NET 8, on a 4-core Linux machine.

Sources

Source What it gave
Valve, Source Multiplayer Networking 66 ticks a second (30 in Left 4 Dead); 20 snapshots a second; 100 ms interpolation so one lost snapshot is covered; a second of history for lag compensation; "command execution time = server time − latency − interpolation"
Yahn Bernier, Latency Compensating Methods in Client/Server In-game Protocol Design and Optimization (2001) Prediction by re-running unacknowledged commands with shared movement code; rewinding also alive/dead and posture; projectiles as the hard case
Gabriel Gambetta, Fast-Paced Multiplayer I to IV Sequence numbers and reconciliation; showing other players in the past; lag compensation's "shot after taking cover"
Glenn Fiedler, gafferongames.com (fixed timestep, deterministic lockstep, snapshot interpolation, reliable ordered messages, floating-point determinism) Lockstep for 2 to 4 players at most; interpolation delay of 3 send intervals at 2 to 5% loss; redundant sending; determinism only with the same build and instruction set
GGPO (MIT) Rollback needs a deterministic simulation that saves, loads and re-runs frames; at most 8 frames predicted; SyncTest
Riot, Peeking into VALORANT's Netcode 128 ticks; buffering costs latency; 10 ms of peeker's advantage swings duels from 90% to 10%; capped rewind
Tim Ford, "Overwatch Gameplay Architecture and Netcode" (Blizzard; GDC 2017 implied, video, read as a transcript; notes) 60 Hz command frames of 16 ms (7 ms in the tournament configuration) from an accumulator with a carried remainder; the client ahead by half the RTT plus one buffered frame (160 ms RTT: 80 + 16 ms); a starved server repeats the last input and tells the client, which dilates time (no percentage given); every input since the last acknowledged state in each packet; above about 220 ms RTT hit impacts wait for the server, the rewind is clamped and targets past it are extrapolated; favour the shooter unless the victim mitigated; positions quantised to 1/1024 m
Godot 4.7.2 class reference and source (enet_multiplayer_peer.cpp), high-level multiplayer, dedicated servers The API in the skill; the create_server argument bug; no lag simulation in ENet
Mark Frohnmayer and Tim Gift, "The TRIBES Engine Networking Model" (Dynamix, GDC 2000; supplied as a PDF) Data by delivery need (not guaranteed, guaranteed, most recent state, guaranteed quickest); a connection layer that notifies per packet and never retransmits (about 3 bytes a packet); ghosts with per-client state masks where a lost bit is resent only if no later packet carried it; scoping and priority per client; moves sent in 3 consecutive packets after resending every unacknowledged move caused a feedback loop on modems; datablocks sent once; 32 ms ticks with rendering interpolated between them; guaranteed events in the same packets, a lost packet's events pushed back onto the head of the queue; data packed into the fewest bits; each client sets the packet rate and size it can receive
"Replicating Chaos: Vehicle Replication in Watch Dogs 2" (Ubisoft Toronto, GDC; speaker transcribed as Matt Del Bosque), notes in gdc/watch-dogs-2-vehicle-replication.md Fast objects: extrapolation along a curve from the yaw rate, projective velocity blending (formula from Curtiss Murphy, Game Engine Gems 2, 2011), one time offset that interpolates inside the snapshot buffer and extrapolates past it; the C# example's Net/DeadReckoning.cs and DeadReckoningTests
David Aldridge (Bungie), "I Shot You First: Networking the Gameplay of Halo: Reach" (video; notes in gdc/halo-reach-networking.md) A fixed cap on objects that jump the priority queue per packet; background data at most 10% of a packet; predict the animation, let the host decide the outcome; "ask where you're hiding the lag"; a host-side wind-up shortened by the round trip; warp logic, blend visuals (vehicles from 15 to 20 updates a second down to 5)
Michael Stallone (NetherRealm), "8 Frames in 16ms: Rollback Networking in Mortal Kombat and Injustice 2" (GDC; notes) 3 fixed frames of input delay plus 7 of rollback, 10 frames or 333 ms of round trip, over 99% of their matches, then a net pause; rolling back 7 frames every frame as the debug mode; saving only the confirmed frame, and speculative saves that cut rollbacks by 30%; the beta's feedback loop (one player at 7 frames every frame) and slowing the peer ahead by more than about 1.5 frames, at most about 2 ms a frame; no rollback across camera cuts; desyncs under 0.1%
netfox (MIT), Netfox Sharp (MIT, experimental) Rollback and prediction nodes for Godot; RigidBodies cannot be rolled back because Godot cannot step physics by hand; its network simulator works only from the editor
Fletcher Dunn (Valve), "Denial of Service Mitigation" (GDC talk; notes) A process fails on packet rate before bandwidth: well-written code copes to ~250,000 packets a second, naive code at 100 to 1,000; junk crowds legitimate packets out of OS buffers; never reveal player IPs; relay game traffic (Steam Datagram Relay) so servers have no public address; backend over HTTP; bans need a re-entry cost. Fed the soak -JunkPps flood and anti-cheat's denial-of-service section
GDC talks folded in (notes): Spelunky 2's propagation netcode (Blitworks), Destiny's mission architecture and Destiny 2's crossplay (Bungie), For Honor's deterministic and replicated AI (Ubisoft Montreal), The Maestros' indie backend, Sony's online crash course 8% of players with connection cuts over 3 s and 32 of 36 teleport cases caused by other programs' notifications; a host migration every 160 s per player; AI time-sliced to about 3 Hz inside rollback, up to 9 re-simulated steps a frame at 300 ms; whole-state lobby messages; crossplay pools by platform security and blocks across linked accounts. In multiplayer's sessions.md and rollback-field.md

Measurements

What Result
ENetMultiplayerPeer.CreateServer(port, 256, 3) on 4.7.2 Inputs stalled about 350 ms at a time, 13 to 17 sequence gaps in 3 s, reliable RPCs included; (port, 256) had none. The source passes max_channels + 2 as the incoming bandwidth limit; Godot's master branch has the arguments in the right order
Headless Godot with no frame cap About 144 frames a second; Engine.MaxFps = 60 used 30 to 40% less CPU
tc qdisc ... netem in this container Not available (the kernel lacks sch_netem); bandwidth shaping only
A lag-simulating MultiplayerPeerExtension around ENet, server and 4 clients, 15 s RTT 0.25, 76, 132 and 297 ms at the four settings; 0 corrections with inputs repeated three times, 144 (up to two steps) with one copy at 5% loss; 0 checksum mismatches
A server and 50 clients as 50 processes (60 Hz inputs, 30 Hz snapshots, about 2 minutes) About 95% of one core in all with Engine.MaxFps = 60 (155% without); about 163 MB per process, 8 GB in all; 24 KB/s down and 2 KB/s up per client
The same 50 clients in one process (a SceneMultiplayer per branch) 16% of one core, 184 MB
250 players, full-world snapshots Breaks: 4 KB snapshots fragment, reliable round trips reach 18 s. Full-world snapshots grow with the square of the players; past about 80 to 100, send only what each player needs (interest management) and deltas
The first seconds after 50 clients connect at once 385 inputs lost in one 30-second run, round trips up to 2 s: keep the warm-up out of any assertion
One process, a server and 50 clients, 25 ± 10 ms each way and 1% loss, 5 minutes Memory 179 to 180 MB, about 47% of one core, 0 corrections, 0 checksum mismatches
Godot 4.7.2 mono quitting 2 of about 60 processes crashed on exit (signal 11) and lost buffered output: write results to a file before quitting
Godot 4.7.2 class reference (GodotSharp.xml), SceneReplicationConfig.ReplicationMode Always: "constantly sending updates using unreliable transfer mode"; OnChange: "sending updates using reliable transfer mode when its value changes"; visibility per peer, no priority or byte budget
The C# example's TRIBES model (ReplicationTests): 80 objects, 4 state groups, 8 changes a tick, 30 packets a second of at most 400 bytes, 20% loss, 120 ± 30 ms, 900 ticks then 120 quiet 212 of 1,020 packets lost; the client ends with exactly the in-scope objects and current values; 0 older values applied; 51,084 bytes against 1,005,540 for full snapshots of the scope (5.1%), both at 4 bytes a value; 1,366 lost values resent, 152 stale ones not
The same replicator, 40 objects with priorities 1 to 40, room for 5 a packet, all changing every packet, 992 packets Updates sent: priority 40, 241; priority 20, 124; priority 1, 6: bandwidth follows priority and nothing starves
The same replicator, an 8-player fight and 200 objects entering scope at once, 400-byte packets Uncapped: the opponent got no update for 9 packets in a row, all created within 10; MaxOverridesPerPacket 4: none missed, all created within 11
The C# example's event stream (EventStreamTests): 60 objects appearing over 600 ticks, spawn events posted a tick after the object enters scope, guaranteed hits and unguaranteed footsteps, 400-byte packets (100 for events), 20% loss, 120 ± 30 ms, 150 quiet ticks 750 packets, 141 lost; every guaranteed event processed exactly once and in order, 85 resent, 163 unguaranteed lost and never resent; events ignoring their ghost (as a separate reliable channel would): 11 processed before the object existed on the client; waiting for the ghost's creation: 0
The C# example's bit packing (BitPackerTests) A position in -1024..1024 m at 1/1024 m takes 22 bits, worst error 1/2048 m; position, health 0..100, a 16-state animation and a flag: 56 bits, 7 bytes, against 20 at 4 bytes a value. GhostFormat sends a ghost's groups at those widths: its creation 14 bytes with the 4-byte header, against 27; a health change alone 8 against 12
LinkBudget (ReplicationTests): 120 objects changing every tick at 30 ticks a second, 20,000 bytes a second of uplink; the client asks for 20 packets of 300 bytes, then 10 of 120, then 60 of 2,000 Exactly 20, 10 and 30 packets every second; never a packet over the request; 5,940, 1,180 and 19,913 bytes a second (the last capped to 666-byte packets by the uplink)
The C# example's netcode model at 50, 100 and 250 ms with jitter and 2% loss (NetcodeTests) Clients end exactly on the server's state; no input lost; interpolated steps of one tick; every rewound hit registers, none without rewinding
The C# example's rollback session (RollbackTests, 3 + 7 frames, 3,600 ticks) at 50, 100 and 250 ms (jitter 5, 10, 15 ms, 2% loss) and 333 ms (none) No pause; every confirmed checkpoint equals the same match played offline; 0, 146, 295 and 300 rollbacks for 301 remote input changes, mean depth 0, 1.01, 4.92 and 7.00, never above 7
The same, the first round trip that pauses (10 ms steps, no jitter or loss) 340 ms for 3 + 7 frames (the window is 333 ms), 200 for 2 + 4 (200), 300 for 1 + 8 (300). At 340 ms 327 of 3,600 ticks paused; at 400 ± 20 ms with 2% loss, 829; both stay in sync
Time sync: 190 ± 4 ms, one peer started 4 frames ahead, 3,000 ticks after a 600-tick warm-up Without: the peer ahead re-simulates 7.00 frames a rollback, the other 0, and stays 4.00 frames ahead. With: 3.00 and 3.00, 0.00 ahead, after stretching 34 ticks by 2 ms (66.7 ms, 4 frames)
Save policy: re-simulated frames a tick, rollbacks (with nothing wrong), worst tick in steps + saves; every frame against confirmed frame 150 ms: 0.17, 300 (0), 4 + 4 against 1.21, 735 (435), 8 + 1. 250 ms: 0.41, 298 (0), 7 + 7 against 2.40, 1,277 (979), 8 + 1. 333 ms: 0.58, 300 (0), 8 + 8 against 6.98, 3,590 (3,290), 8 + 1
A value refreshed every 8 frames into a field Save() misses (PoseCacheSim) SyncTest depth 1 passes 3,600 ticks; depth 7 reports tick 3, found while re-running ticks 2 to 8 after the refresh at tick 8; a rollback session at 250 ms desyncs from the offline match
The same at 250 ± 25 ms, 4 players, a 10 s storm of 4 packets in a row lost every half second, then calm Waiting server: 0 corrections, moving players left standing 930 times. Repeating the last input at a fixed rate: 3,441 repeated (887 in the calm), 291 corrections, about 0.15 inputs queued. With time dilation of 2 / 5 / 10%: 246 / 211 / 250 repeated (16 / 43 / 50 in the calm), 34 / 33 / 35 corrections, 3.0 / 3.2 / 3.3 inputs queued late in the storm, 1.1 / 2.1 / 2.8 in the calm; 1%: 345 repeated; 20%: 395, and 4.0 queued in the calm
10 packets in a row lost 4 times at 250 ms, 2 players 3 copies of each input: 64 inputs lost, 6 corrections. Every unacknowledged input (capped at 32): 0 lost, 0 corrections, 18.5 inputs a packet on average, 30 at most
A 250 ms rewind cap A 500 ms client's shots rewound 553 to 643 ms without it, all hits; with it 0 of 16 hit. A 50 ms client (150 ms back) is unaffected, 16 of 16
soak on tests/soak-fixture, a server and 4 clients for 30 s (standard 4.7.2) Each process about 95 MB, flat; under 2% CPU each; round trip about 7 ms
soak with --leak (16 KB a tick never freed) and --crash-after=5 soak-leak at about 1700 MB/h; soak-failed naming the server's exit code 3
soak -JunkPps <n> on tests/soak-fixture, a junk-UDP flood against the server's own port on 127.0.0.1, 4 clients, 20 s (standard 4.7.2, 4 cores) None and 1,000 and 10,000 pps: input loss 0.1 to 0.6%, worst round trip 7 to 24 ms, all clients fine. 50,000 pps: input loss about 38%, round trips up to 5.9 s, a client dropped, soak-failed. 100,000 pps: input loss 50 to 57%, round trips 0.3 to 1.2 s, one or two clients dropped, soak-failed
Where the Godot 4.7.2 ENet server degrades (same run) The cliff is between 10,000 and 50,000 junk packets a second: one poll draining at most 256 datagrams per service call, on the main thread, copes with tens of thousands a second, then collapses. Between the talk's "naive 100 to 1,000" and "well-written ~250,000". Derived, this machine and fixture

Not confirmed