Quick answer: A network-only bug hides on localhost because loopback has zero latency, no packet loss, and a single client. To reproduce it, introduce artificial delay and jitter, run multiple real clients, and capture state from both the client and server at the moment the symptom appears. Compare the two timelines to find the divergence.

Some bugs only exist on the network. They never appear when you press play in the editor, because your loopback connection delivers every packet instantly and in order, and there is only one client in the world. The moment a real player connects from a phone on cellular, with another player acting at the same instant, the timing assumptions in your netcode fall apart. Reproducing these bugs is mostly about recreating the conditions that expose them, then capturing enough state from both sides to see where the two machines disagreed. This post walks through how to do that reliably.

Why localhost lies to you

On localhost every packet arrives in roughly a microsecond, in the exact order you sent it, and nothing is ever dropped. Real networks do none of that. Packets arrive late, out of order, in bursts, and sometimes not at all. Code that implicitly assumes a message arrives before the next frame works perfectly in the editor and then desyncs in the wild. The first mental shift is accepting that your development environment is the least realistic test bed you own, and that passing there proves almost nothing about network behavior.

The second trap is client count. Many bugs only surface when two clients act on the same entity within the same server tick, or when a late join happens mid-action. A single client can never trigger a race between two players. So before you can reproduce anything, you usually need at least two real clients connected through a real, degraded link, both driven through the same sequence of actions. Until you have that setup, you are guessing.

Simulate latency, jitter, and loss

Start by adding artificial network conditions. Most engines expose a network simulator: Unity has the transport simulator pipeline, Unreal has Net Emulation profiles, and at the OS level you can use clumsy on Windows or tc and netem on Linux. Dial in a base latency of 80 to 150 milliseconds, add 30 to 50 milliseconds of jitter, and set packet loss to a few percent. These are ordinary mobile numbers, not extreme ones, and they expose the majority of timing bugs that loopback hides.

Once the symptom appears under simulation, treat the simulator settings as part of the repro. Write them down: latency, jitter, loss, and which client experiences them. A bug that only reproduces at 200 milliseconds one-way is telling you something specific about a timeout or interpolation window. Sweep the values to find the threshold where it appears and disappears. That threshold is often a direct pointer to the constant in your code that is wrong, such as a buffer size or a reconciliation horizon that is too short.

Capture both ends at once

A network bug is a disagreement between two machines, so a log from one machine is half a story. You need synchronized capture from both the client and the server: the input the client sent, the tick it sent it on, what the server received and when, and the resulting authoritative state. Tag every relevant message with a sequence number and a tick index so you can line the two logs up afterward. Without that shared coordinate system, you cannot tell whether the client mispredicted or the server processed things out of order.

When the symptom fires, snapshot the full relevant state on both sides: position, velocity, health, the pending input buffer, and the last acknowledged tick. The divergence almost always shows up as a single field that differs between client and server at a known tick. Finding that field and that tick is the whole game. Everything before it is setup, and everything after is just confirming the fix holds under the same simulated conditions that surfaced the bug.

Make the repro deterministic

A bug you can only reproduce one time in twenty is barely reproducible at all. Push toward determinism. Script the client inputs instead of pressing keys by hand, so both clients execute the exact same actions on the exact same frames every run. If your simulation supports a fixed random seed, pin it. Replace wall-clock timing with your fixed tick so the only remaining variable is the network layer you are deliberately degrading. The closer you get to a single button that triggers the bug, the faster you iterate.

Record the scripted session so you can replay it. A short recording of inputs plus the simulator profile becomes a regression test: after you ship a fix, you replay the same session and confirm the divergence is gone. This turns a flaky field report into a repeatable engineering artifact. It also lets a teammate reproduce the bug on their machine without you narrating the steps over a call, which is the difference between a bug that gets fixed this week and one that lingers for a month.

Setting it up with Bugnet

Most network bugs reach you first as a confused player report, not a clean repro. Bugnet's in-game report button captures the surrounding state automatically when a player hits the problem, so instead of just hearing the game broke you get the player attributes, platform, connection context, and a snapshot of what was happening. For a network-only bug that context is gold: it tells you the player was on mobile data with two other players in the zone, which is exactly the condition you need to recreate in your simulator. You stop guessing at the repro and start matching it.

When the same desync hits many players, Bugnet's occurrence grouping folds the duplicate reports into one issue with a running count, so you can see at a glance that 140 players hit this and only nine hit that. Custom fields let you record the simulated latency threshold and the diverging tick right on the issue, and player attributes let you filter to just the high-latency reports. One dashboard holds the original reports, your repro notes, and the eventual fix, so the link between symptom and conditions never gets lost.

Lock the fix in and move on

After you have a fix, do not trust a single passing run. Re-run the scripted, degraded session several times, then sweep the simulator values around the threshold you found earlier to confirm the bug does not reappear just outside your test point. Network bugs love to move rather than die: a fix that works at 150 milliseconds can fail at 250. Test the neighborhood, not just the spot. Then add the recorded session to your regression suite so a future change cannot quietly reintroduce the same divergence.

Finally, write down what the real-world condition was, in plain language, next to the technical fix. Future you will read a one-line note like two clients acting on the same loot within one tick under high latency and immediately understand the class of bug. That habit slowly builds a map of your netcode's weak spots, and over time you start designing against them instead of rediscovering them one painful field report at a time. Reproducibility is the skill that makes the rest of network debugging possible.

Localhost is the least realistic network you own. Degrade the link, run two clients, and capture both ends before you trust any repro.