Debugging Desyncs

March 22, 2026 · View on GitHub

A desync means two clients have diverged — the same inputs have produced different game states. This guide covers how to detect and diagnose desyncs.

SyncTest Session

The easiest way to catch desyncs during development is to use SyncTestSession instead of P2PSession. It simulates rollback locally by re-running the last check_distance frames every update and comparing checksums.

let session = SessionBuilder::<GgrsConfig>::new()
    .with_num_players(1)
    .unwrap()
    // re-simulate 7 frames back every update and compare checksums
    .with_check_distance(7)
    .add_player(PlayerType::Local, 0)
    .unwrap()
    .start_synctest_session()
    .unwrap();

A SyncTestMismatch trigger fires if checksums diverge. You can observe it:

app.add_observer(|trigger: On<SyncTestMismatch>| {
    error!(
        "Desync detected! Frame: {}, mismatched frames: {:?}",
        trigger.event().current_frame,
        trigger.event().mismatched_frames
    );
});

Adding Checksums

bevy_ggrs only detects desyncs for state that is checksummed. Register checksums alongside your rollback state:

app.rollback_component_with_copy::<Health>()
   .checksum_component_with_hash::<Health>();

app.rollback_resource_with_clone::<Score>()
   .add_plugins(ResourceChecksumPlugin::<Score>::default());

The more state you checksum, the more precisely SyncTest can locate a desync.

Common Causes

Non-deterministic query order — See pitfalls.md. The most common cause of desyncs.

Unregistered components — An entity is resimulated without all its components, changing behavior. Register everything that affects gameplay.

Local<T> in rollback systems — Diverges between original and resimulation. Use a snapshotted resource instead.

Floating-point non-determinism — Rust's f32/f64 arithmetic is deterministic on the same platform, but not across different CPUs or operating systems. If you need cross-platform play, consider fixed-point math.

Randomness — If your game uses rand, seed the RNG from confirmed game state and register it for rollback. Never use OS randomness inside GgrsSchedule.

Time-dependent logic — Do not use Res<Time> inside GgrsSchedule. Use RollbackFrameCount to track logical game time instead.

P2P Desync Detection

For P2P sessions, enable desync detection via GGRS:

SessionBuilder::<GgrsConfig>::new()
    .with_desync_detection_mode(ggrs::DesyncDetection::On { interval: 10 })
    // ...

When a desync is detected, GGRS fires a GgrsEvent::DesyncDetected event containing the local and remote checksums and the frame number. You can observe this via the Session resource.

Known Limitations

  • Snapshot access during desync: Snapshots are pruned as frames are confirmed, which happens before desync detection is reported. It is not currently possible to inspect the snapshot of the diverging frame directly.
  • SyncTest does not emit GgrsEvent::DesyncDetected: It fires SyncTestMismatch instead (see above).