Quick answer: Every save includes a monotonic game-time timestamp, a content hash, and a progress metric. On sync, the higher progress metric wins when both saves contain real advancement. Write a .bak before every overwrite. Prompt only when the rule is genuinely ambiguous, and log every conflict decision for post-mortem.
A player finishes a six-hour session on their desktop. They fire up Steam Deck the next morning, and the last three hours are gone. The thread on your support forum already has forty-two replies, each angrier than the last. Cloud save conflicts are one of the most damaging bugs you can ship — trust, once broken, doesn’t come back. The defense is a save system that treats conflict as a first-class state, not an edge case.
What Cloud Sync Actually Does
Every platform’s cloud save is essentially the same primitive: on launch, the client checks for a newer version of the save on the server. If present, it downloads and replaces the local file. On exit (or on demand), it uploads the local file. Each platform has a few wrinkles:
- Steam Cloud: uses file path and timestamp. Quota-based; huge saves require opt-in.
- PlayStation Plus: uploads are explicit (game-triggered) and size-limited. Conflict is detected server-side.
- Xbox Live / Microsoft Store: Connected Storage handles sync; conflict returns a
GameSaveProviderFailureyour code must resolve. - Cross-platform (your own cloud): whatever behavior you implement.
Each platform’s default conflict-resolution policy is wrong for your game. They decide based on file timestamps, which are unreliable when players change timezones, travel across DST boundaries, or have a dead CMOS battery. Your own logic must sit on top.
Save Header Fields You Need
Every save file starts with a header containing the data your sync logic needs. Minimum:
struct SaveHeader {
uint32 version; // schema version
uint64 gameTimeSeconds; // monotonic in-game time played
uint64 wallClockUnix; // for human display only
uint8 contentHash[32]; // SHA-256 of save body
float progressPercent; // 0..1 of total completable content
char deviceId[32]; // which device wrote this
uint32 saveCount; // monotonic counter per user
};
Monotonic game time and save count are the reliable signals. Wall clock is useful only for showing the player “last played 2 days ago” and must never be the sole basis for conflict resolution.
The Resolution Algorithm
On launch, load both the cloud save header and the local save header. Run this decision tree:
- If hashes are equal, the saves are identical. Use either.
- If one save is missing, use the other.
- If the save counts differ by more than 1, you have a multi-device branch. Prompt the user.
- Otherwise, use the save with the higher
gameTimeSeconds— the one that represents more play.
The “branch” case catches the scenario where both devices played from the same starting save, each accumulated progress, and neither knew about the other. That’s the only case where the player must decide, because either choice loses data.
The Prompt UI
When you do prompt, don’t just say “Cloud save or local save?” Show useful metadata:
- “Cloud save: 14h played, 38% complete, last device: Deck”
- “Local save: 11h played, 31% complete, last device: Desktop”
- A “keep both” option that saves the loser to a recoverable slot.
The “keep both” option is crucial. Players who pick wrong should have a path to recovery. Persist the discarded save under a versioned name (savegame.conflict.2026-04-10.dat) so they can recover from the in-game menu.
Backups Are the Safety Net
Before every overwrite, write the previous file to savegame.bak. Keep the last three backups. A rotation scheme like .bak.1, .bak.2, .bak.3 is fine. Cloud sync goes wrong occasionally — driver crashes mid-upload, corruption, user intervention — and the backup is the difference between a restorable mistake and a Reddit thread.
Surface the backups in the UI. A “Restore previous save” option under settings is a trust-building feature. Don’t hide it; players who need it are already in a bad state.
Detecting Silent Failures
The worst bugs are syncs that never happen. Upload errors that the platform swallows. Files that aren’t marked dirty. Save directories that aren’t in the cloud sync path. Instrument every step:
- Emit an event on every save-to-disk with the file size and hash.
- Emit an event on every upload attempt with the outcome (success, retried, failed).
- Emit an event on every conflict resolution with which side won and why.
In your dashboard, alert when the ratio of saves-written to uploads-succeeded drops below 0.95. That ratio catches cloud sync outages, bad quota behavior, and users who have disabled cloud sync without realizing it (then complain when the data’s gone).
Testing the Unhappy Paths
Write integration tests for each of these scenarios:
- Newer cloud, older local (normal resume).
- Older cloud, newer local (offline session, online reconnect).
- Cloud save corrupted (fall back to local, surface a warning).
- Local save corrupted (download cloud).
- Both saves present with diverged branches (prompt).
- Save count reset (player reinstalled).
Each scenario must either resolve silently with a correct result or prompt with clear information. No scenario should silently lose data. That’s the bar.
“Cloud sync is distributed systems in miniature. Assume clocks lie, networks partition, and players do every combination of actions you didn’t plan for.”
Related Issues
For Steam-specific save integrations, see how to integrate Steamworks for indie games. For diagnosing save-related crashes, see how to reproduce a bug from a save file.
Players forgive bugs. They don’t forgive lost progress — so the save system is the last system you should ship without a full test matrix.