Quick answer: Record every client input and every server snapshot tick to a replayable log, and when a player reports a missed shot, replay those logs against the actual server code. Lag compensation bugs cannot be reproduced from screenshots or verbal descriptions — you need the exact tick history.
A player fires a shot that they clearly saw hit an enemy in their crosshair. The server says they missed. The player posts a clip with "hitreg is broken" in the title and it gets 800 upvotes. You try to reproduce locally and the game works perfectly. This is the most frustrating class of multiplayer bug and also the most common one in shooters, fighting games, and any game with hit detection across latency. Debugging it requires a specific workflow: you have to capture exactly what the server saw, then replay it.
A Brief Lag Compensation Refresher
In a lag-compensated game, the client sends an input like "I fired my gun at tick 1000 pointing at angle (45, 20)". The server receives this at, say, tick 1008 — the client is 80 ms behind the server clock due to network latency. Without lag compensation, the server would check hitboxes at tick 1008, where the target has moved, and the shot misses. With lag compensation, the server rewinds the world state to tick 1000, performs the hit check, and then unwinds time forward again.
This works beautifully when everyone is on ping under 50 ms. It falls apart when:
- The server's snapshot history buffer is too short to rewind to the requested tick
- The client's timestamp is wrong, spoofed, or clamped
- Interpolation causes a mismatch between the position the client rendered and the position stored in the snapshot history
- A hitbox is made of multiple colliders that do not all rewind together
- The ping estimate used to calculate rewind time is stale
Step 1: Record Everything
Lag comp bugs are impossible to debug without a full recording. On the server, log every input received from every client with its tick and a wall-clock timestamp, and log every snapshot you send out. The format is append-only binary — you will write a lot of it.
type RecordedInput struct {
PlayerID uint32
ServerTick uint32 // server tick when received
ClientTick uint32 // tick client claims input happened on
WallClock int64 // nanoseconds, for correlating with logs
Buttons uint32
AimYaw float32
AimPitch float32
}
type RecordedSnapshot struct {
ServerTick uint32
Players []PlayerState // positions, orientations, hitboxes
}
func (r *Recorder) RecordInput(inp RecordedInput) {
binary.Write(r.inputFile, binary.LittleEndian, inp)
}
Store the recording per match, indexed by match ID. A 10-minute match of 10 players at 60 Hz is around 50 MB of binary data — cheap to store, invaluable to debug.
Step 2: Correlate With Reports
When a player submits a bug report, they need to provide enough information for you to find the match in the recording storage. Include in every in-game bug report form:
- Match ID (auto-filled from the current session)
- Server tick or wall-clock time at the moment of the bug (auto-captured)
- Player ID (auto-filled)
- Description of what happened from the player's perspective
If you can attach a short video clip (last 10 seconds) that is even better — it gives you a visual ground truth for what the player saw.
Step 3: Build a Replay Tool
The replay tool loads the recorded inputs and snapshots for a specific match and walks through them tick by tick against the actual game logic. At any given tick you should be able to see:
- The world state as the server recorded it
- The world state as the player would have seen it (interpolated back by their ping)
- Every rewound hitbox used for lag-compensated hit tests
- The exact result of each hit test
This is the critical piece. Build a simple 3D viewer (or 2D, depending on your game) that overlays:
- The player's viewpoint (green frustum)
- The target's server-tick position (red box)
- The target's rewound position used for hit check (orange box)
- The target's client-interpolated position (blue box, estimated)
- The shot ray as traced on the server (yellow line)
When the player's report says "I clearly hit them", you load the replay to the reported tick and look at where these boxes are. Seven times out of ten, the orange rewound box is not where the player thought the target was. Once you can see the discrepancy, the fix usually becomes obvious.
Step 4: Diagnose the Common Causes
Rewind time out of range. Your snapshot history might be a 1-second ring buffer (60 ticks at 60 Hz). A player with 300 ms ping plus 200 ms of jitter might request a rewind to 500 ms ago, which is outside the buffer. The server snaps to the oldest tick in the buffer, which does not match the player's view. Fix: extend the buffer to 2 seconds minimum.
Client tick clamping. Some anti-cheat logic clamps the client's reported tick to a maximum acceptable offset from the server. If you clamp too aggressively, legitimate shots from high-ping players get compensated against the wrong tick. Log every clamp event and check whether your threshold is too tight.
Hitbox interpolation offset. If your client renders the enemy interpolated 100 ms behind the current client time (for smooth motion), but your server rewinds to the client's current tick, you have a 100 ms offset between what the player saw and what the server tested. The server should rewind to client_tick - interpolation_offset, not just client_tick.
Composite hitboxes. A character made of a torso, head, and limbs may have each part tracked as a separate body. If any of them are not included in the rewind, you get impossible hits (or impossible misses).
Ping estimate lag. Some games use an exponential moving average of round-trip time as the compensation amount. If ping suddenly spikes, the moving average lags behind and compensation is wrong for several seconds. Use a more responsive estimate (median over last 10 packets) or compute compensation per-input from the actual tick delta.
Step 5: Add Sanity Check Metrics
Instrument every hit test with metrics that catch anomalies in aggregate:
metrics.Histogram("hit_rewind_ms", rewindMs)
metrics.Histogram("hit_result", hit, map[string]string{
"weapon": weapon,
"distance_bucket": distanceBucket,
})
metrics.Counter("hit_rewind_buffer_miss").Inc()
Plot these in your dashboard and watch for:
- Rewind buffer misses above 0.1% of shots — your buffer is too small
- A sudden drop in hit rate — a patch regressed the compensation math
- Distance-correlated hit rate changes — your hitboxes are mis-scaled at range
Step 6: Close the Loop With Players
When you identify a specific report as "worked as intended" or "bug confirmed", reply to the reporting player with a clip or diagram from your replay tool. Players are much more forgiving when they can see exactly what happened — "You aimed here, the target was actually here, your ping was 180 ms and our server rewound to 150 ms ago" — than when they get "not reproducible" as a closing note.
"The best multiplayer networking team I ever worked on had a replay tool that could load any reported shot and overlay it on the server's view. It was the only thing that let us ship lag compensation without constant player rage."
Related Issues
For general multiplayer desync debugging see how to debug multiplayer desync issues. For network packet loss that often compounds lag compensation problems, read how to debug network packet loss.
Record everything. You cannot debug what you did not capture.