Quick answer: Document your conflict resolution policy in writing, build a test harness with two simulated devices, force divergence by disconnecting one before both modify the save, and verify the merge result against the policy. Test the edge cases (same timestamp, clock skew, mid-merge failure) explicitly.
A cloud save conflict is rare. A player has to play on two devices, accumulate state on each, and have one of them be offline at the wrong moment. Once it happens, though, the consequences are catastrophic: lost progress, restored items, deleted characters, broken trust. Most studios discover their conflict resolution code is broken six months after launch when a player’s 80-hour save vanishes. Here is how to find those bugs in the test harness instead.
Why Conflicts Happen
A player launches your game on their PC. Their save is at version 47 in the cloud and version 47 on the PC. They play for two hours, the PC saves locally and pushes to cloud, the cloud is now at version 48.
Later, the player launches on their Steam Deck. The Deck’s local save is at version 47 (from a previous session) but the cloud is at version 48. The cloud sync pulls the newer cloud version and the Deck plays from version 48. So far, fine.
Now imagine the same scenario but the Deck is in airplane mode. The Deck has version 47, no network, and plays for an hour. It saves locally to version 48 (Deck’s local view). When it comes online, the cloud is also at version 48 (PC’s view) but with completely different content. Both saves are the same version number, both are valid, and they cannot both be the canonical save.
This is a conflict. Your code must handle it.
Step 1: Write Down the Policy
You cannot test a policy you have not written. The policy must specify, for every field in your save:
- Conflict resolution strategy: latest-wins, max-value, accumulate, or ask-player.
- Whether the field is mergeable (currency totals, XP) or atomic (inventory, current quest state).
- What happens if the field is missing in one of the saves.
An example policy:
player_xp: max-value (use the higher of the two)
gold: latest-wins (the newer save's gold value)
inventory: latest-wins (atomic; do not merge items)
quests_completed: union (merge both lists)
current_quest: latest-wins
unlocked_levels: union
play_time_seconds: max-value
cosmetics_unlocked: union
character_name: latest-wins
settings: latest-wins (per-device anyway)
The strategy depends on the data’s semantics. XP should never decrease, so max-value. Inventory is a single state machine that should not be partially merged, so latest-wins. Cosmetics are additive permissions, so union.
Step 2: Build a Test Harness
You need two simulated “devices” that can both push to and pull from a single cloud save backend. In test, the cloud backend can be a directory on disk — the API surface is what matters, not the storage.
// Conceptual test harness
class CloudSaveTest
{
Device deviceA = new Device("Alice-PC");
Device deviceB = new Device("Alice-Deck");
FakeCloud cloud = new FakeCloud();
[Test]
void XPMaxWins()
{
// Both devices start with version 1, xp = 100
var save = new SaveData { version = 1, xp = 100 };
cloud.Push(save);
deviceA.Pull(cloud);
deviceB.Pull(cloud);
// Disconnect both, modify locally
deviceA.save.xp = 250;
deviceA.save.version = 2;
deviceB.save.xp = 180;
deviceB.save.version = 2;
// A reconnects first and pushes
deviceA.Push(cloud);
// B reconnects and gets a conflict
var resolved = deviceB.Sync(cloud);
// Policy: max-value for xp
Assert.AreEqual(250, resolved.xp);
}
}
Each test simulates a specific conflict scenario and asserts the resolved save matches the documented policy. Run the suite on every release.
Step 3: The Edge Cases
The hard bugs are not in the obvious cases. They are in the corner cases that nobody thinks to test.
Same timestamp. Two devices saved within the same second. Whose wins? Pick a deterministic tiebreaker (device ID alphabetical, or random with a fixed seed) and document it.
Clock skew. A device with an incorrect system clock writes a save with a timestamp ten years in the future. Your latest-wins policy now considers it the “newest” forever. Validate timestamps against the cloud’s server time and reject anything more than a few minutes in the future.
Partial sync failure. The push uploads the save but the response is dropped. The device thinks the push failed and retries. Your backend now has two nearly-identical pushes from the same device. Use idempotent writes keyed by client-generated UUIDs.
Old client, new save format. A new build adds a field. An old client downloads a save that contains the field, drops it on read, and pushes back a save without it. The new field is lost on the next sync. Either preserve unknown fields verbatim or version the entire save and refuse downgrades.
Recoverable corruption. The save downloads but its checksum fails. Roll back to the last known-good local save instead of overwriting it. Never trust corrupted data.
Step 4: Real Devices
The harness catches logic bugs. It does not catch network bugs. Once the harness passes, do a real-device test on every supported platform:
- Play on Device A, push to cloud.
- Switch to Device B, pull from cloud, play, push.
- Disconnect B’s network. Play on B for 10 minutes.
- Connect to A, play for 10 minutes, push.
- Reconnect B’s network. Watch the conflict resolution UI fire.
- Verify the merged save matches what you expected.
Do this on every storefront integration: Steam, GOG, Xbox cloud, PlayStation cloud, App Store. Each one has different timing characteristics and a different chance of producing conflicts in the wild.
The Player-Facing UI
When a conflict happens and your policy is “ask the player,” the UI is the difference between a recoverable hiccup and a panicked refund. The conflict dialog should show:
- Both saves with human-readable summaries (level, location, play time, last played).
- A clear “Use This One” button per side.
- A backup option that exports the unused save to a file the player can keep.
Never silently overwrite. Even when you think the resolution is obvious, the player should at least see what happened.
“A cloud save conflict is the closest thing to a crash that does not crash. It feels like data loss. Test it like data loss — harshly, often, and on every supported platform.”
Related Issues
For broader save data integrity work, see how to monitor save data integrity across cloud sync. For migration between save versions, see how to debug save file migration bugs between game updates. For preventing corruption in the first place, see how to prevent save file corruption bugs.
Always keep one local backup of the previous save. When a sync goes wrong, the backup is the only thing that lets you recover without ruining the player’s day.