Guide

Game save systems explained: serialization, slots, cloud sync, and migration

A save system is how a game remembers player progress between sessions — inventory, quest flags, world state, settings, and the timestamp of the last campfire. Done well, saves are invisible: players quit mid-boss and resume exactly where they left off. Done poorly, a patch bricks every file on disk, or a cloud sync overwrites forty hours with an empty slot. This guide covers what belongs in save data, how to serialize it safely, checkpoint and autosave UX, multi-slot design, cloud persistence, schema versioning across patches, and the edge cases that break multiplayer and procedural worlds.

What to save (and what not to)

Save data should capture player-authored state and world mutations the game cannot reconstruct from a level file and a seed. That usually means: character stats, inventory, quest completion flags, unlocked abilities, faction reputation, opened doors, destroyed props, and difficulty or accessibility settings the player chose.

Do not persist derived or reproducible data unless profiling proves reconstruction is too slow. Cached pathfinding grids, pre-baked lighting, and static mesh transforms from the level loader belong on disk as assets — reloading them on boot is cheaper than bloating every save. Similarly, avoid saving entire enemy AI blackboards if you can respawn NPCs from spawn markers plus a “defeated” bit per encounter ID.

Separate run state from profile state

Most games split persistence into two layers:

  • Profile / meta save — account-wide unlocks, cosmetics, control bindings, audio sliders, language, total play time. One file per user.
  • Run / campaign save — the current playthrough: level index, checkpoint, in-mission objectives. Often multiple slots (three manual saves plus one autosave is a common pattern).

Keeping them separate lets you patch meta progression without touching in-progress campaigns, and simplifies cloud sync (profile is small and merges often; campaign files are large and conflict-prone).

Serialization: JSON, binary, and engine formats

The on-disk format trades readability, size, speed, and tamper resistance.

JSON and text formats

JSON is the default for indie and browser games because it debugs in any text editor, diffs cleanly in version control, and maps naturally to JavaScript objects. Downsides: larger files, slower parse on mobile, and no built-in schema enforcement — a typo in a field name silently drops data unless you validate on load.

{
  "schemaVersion": 3,
  "player": { "hp": 87, "pos": [124.5, 0, -38.2] },
  "quests": { "main_03": "active", "side_blacksmith": "complete" },
  "seed": 928374651
}

Wrap JSON with a top-level schemaVersion integer and run migrations before hydrating gameplay objects. Libraries like zod or hand-written validators catch corrupt partial writes early.

Binary and custom formats

AAA titles and performance-sensitive mobile games often use binary blobs: fixed structs, length-prefixed strings, or engines like Unreal’s FSaveGame / Unity’s BinaryFormatter successors. Binary saves are smaller and faster to parse but opaque without tooling — invest in a debug exporter that dumps human-readable views for QA.

Compression and encryption

Gzip or LZ4 on top of either format helps when saves exceed a few hundred kilobytes (open-world tile deltas, large inventories). Encryption is not real anti-cheat — keys ship in the client — but light obfuscation deters casual editing in competitive single-player leaderboards. Never encrypt without a checksum; bit rot becomes unrecoverable.

Checkpoints, autosave, and manual slots

Save UX shapes how punishing your game feels. Three patterns dominate:

  • Checkpoint saves — automatic at invisible triggers (door thresholds, mission start). Players cannot save scum past a hard encounter unless they exploit backup files. Common in linear action games.
  • Autosave on interval or event — every N minutes, on map transition, or when opening the menu. Pair with a rotating buffer (keep last three autosaves) so one bad write does not erase everything.
  • Manual save slots — player picks “Save 1 / 2 / 3” with thumbnails and play time. Required for RPGs where players want to branch story outcomes.

Atomic writes are non-negotiable: write to save.tmp, fsync, then rename over save.dat. A crash mid-write should leave the previous file intact. On POSIX and modern Windows, rename is atomic; on browser IndexedDB, use a transaction that commits the new record before deleting the old one.

Death and retry flows

Roguelikes often delete the run on death (permadeath) but still persist unlocks to the profile. Souls-likes reload the last checkpoint but keep the world state changes (opened shortcuts persist). Be explicit in UI copy — “Autosave” icons reduce rage-quits when players did not know progress was committed.

Procedural worlds and deterministic replay

When levels are generated from a seed — see procedural generation in games — the save file rarely needs the full terrain. Store the seed, the generator version, and a compact delta of player changes: mined voxels, placed buildings, chest loot rolls already resolved. Regenerate the base world on load, then apply deltas.

If generator algorithms change between patches, bump generatorVersion and either migrate deltas or invalidate old saves with a clear message. Players forgive incompatible saves after a major expansion; they do not forgive silent terrain shifts that bury their base underground.

Schema versioning and migration

Every shipped patch will add fields — new perks, a third currency, renamed quest IDs. Plan migrations on day one:

  1. Read raw save into a version-agnostic tree (JSON object or byte stream with header).
  2. While schemaVersion < CURRENT, apply sequential migrators (v1→v2, v2→v3).
  3. Validate required fields exist; apply defaults for new optional fields.
  4. Hydrate runtime objects (ECS components, scene graph nodes).

Keep migrators in source control forever — players skip patches. A chain from v1 to v7 must still work two years later. Unit-test each step with golden save fixtures checked into the repo.

Renaming and deleting fields

Never reuse an old field name for a different type. Prefer explicit rename maps in migrators (oldQuestId → newQuestId). When removing features, leave deprecated fields readable for one major version, then strip in a later migrator so rollback installs do not crash.

Cloud saves and cross-device sync

Platform services (Steam Cloud, PlayStation Plus, Xbox cloud, Nintendo Save Data Cloud) and custom backends sync save blobs to a user account. The hard part is conflict resolution when two devices play offline:

  • Last-write-wins — simplest; risks silently dropping the longer play session. Acceptable for small profile files.
  • Newest timestamp wins with UI prompt — show both saves’ play time and location; let the player choose. Best for campaign slots.
  • Merge per subsystem — rare; only when domains are independent (settings merge, inventory does not).

Browser games use localStorage (small, synchronous, fragile) or IndexedDB (async, larger quotas). Sync to your server requires authentication and HTTPS; treat saves as opaque blobs keyed by user ID. Rate-limit uploads and store a content hash to skip redundant transfers.

ECS, scene graphs, and load order

In an entity component system, saves usually serialize component data keyed by stable entity IDs — not pointer addresses or array indices that shuffle between sessions. On load:

  1. Load static level geometry and spawn points.
  2. Deserialize entity roster; create entities with saved IDs (or remap IDs through a translation table).
  3. Attach components from saved blobs.
  4. Run a one-frame “post-load” system to rebuild caches (spatial grids, navmesh links, UI bindings).

Order matters when components reference each other — parent/child transforms, equipped item handles, quest givers pointing at NPC entities. Topological sort or multi-pass hydration prevents null references on the first tick after load.

Multiplayer persistence

Shared-world multiplayer (MMOs, co-op hub worlds) cannot rely on a single local file. The server owns authoritative world state; clients cache visuals only. Persist on the server with a database (Postgres rows per character, Redis for hot session state) and snapshot intervals plus write-ahead logs for crash recovery.

Session-based co-op (drop-in/out for a mission) often saves only the host’s campaign file, with guests receiving ephemeral state — document that clearly. Competitive games with rollback netcode typically do not save mid-match; they replay inputs from a agreed start state.

Corruption, backups, and QA

Detect corruption with a CRC32 or SHA-256 footer on each file. On failure, offer previous autosave slots before declaring the run lost. During development, log save/load timings and file sizes — unbounded quest journals are a common bloat source.

QA checklist: kill the process during save, yank power on console kits, fill disk to zero bytes, downgrade to an older build and upgrade again, sync two devices with divergent offline progress, and load saves created on minimum-spec hardware on a high-end PC (floating-point determinism issues are rare in saves but common in replay systems).

Production checklist

  • Split profile saves from campaign/run saves.
  • Include schemaVersion, buildId, and timestamp in every file header.
  • Write atomically via temp file plus rename (or transactional IndexedDB).
  • Keep at least two rotating autosave generations.
  • Ship tested migrators for every schema change; never break the migration chain.
  • Store procedural seeds plus deltas, not full generated worlds.
  • Use stable entity IDs in ECS saves; rebuild derived caches after load.
  • Prompt on cloud conflicts for campaign data; last-write-wins only for small profile blobs.
  • Validate on load; fail with a user-visible error and recovery options, not a silent hang.

Related reading