September 21, 2026 · 7 min read
Versioning an HTML5 Game Without Breaking Player Saves
A safe update strategy for localStorage, IndexedDB, cached builds, and save-data migrations when players may return months after their last session.
Shipping an update is easy when every player starts from a clean install. Browser games rarely get that luxury. A returning player may open a new build while carrying save data written six months ago, a service worker may still hold part of the previous release, and two tabs may briefly run different versions at the same time. Treating those cases as normal deployment conditions — rather than strange edge cases — is what keeps an update from turning into lost progress and support tickets.
Give the build and the save format separate versions
Your release version and your save-schema version answer different questions. The release version identifies the code and assets currently running. The save-schema version identifies the shape of persisted player data. A balance patch can change the release without changing the save format; renaming inventory fields or replacing a quest-state model changes both. Store the schema version inside every save instead of trying to infer it from missing fields later.
- Keep a human-readable build identifier available in the game and in error reports, such as 1.8.3 or a short commit hash.
- Store an integer save version alongside the data, for example saveVersion: 4. Integers make ordered migrations straightforward.
- Never use the browser cache timestamp as a version. It describes a cached response, not the structure of the player's data.
Migrate one version at a time
A single migration from every historic format directly to the newest format becomes difficult to test and easy to break. Use a chain instead: version 1 becomes 2, 2 becomes 3, and so on until the save reaches the current schema. Each migration should do one small, deterministic job and be safe to run only once. This also lets a player skip several game releases without losing the ability to upgrade their save.
- Read and parse the old save without mutating it first.
- Copy it into a new object, apply the next migration, validate the result, then advance the version number.
- Write the migrated save only after every step succeeds. Keep the original value as a temporary backup until the new save has loaded successfully.
- If validation fails, stop and offer a recovery path. Silently replacing the save with defaults is the fastest way to erase a player's progress permanently.
Use stable storage keys and understand the origin boundary
localStorage and IndexedDB belong to an origin, not to a game title. Moving a game between domains, protocols, or subdomains can make the old data appear to vanish because the new page cannot read storage owned by the previous origin. A portal-hosted build and an externally hosted iframe may therefore see different stores even when the game code is identical. Decide the permanent storage key and hosting model before launch; changing either later requires an explicit transfer or account-backed save system.
Use a namespaced key such as studio.game.save rather than save or progress. Do not include the build number in the primary key, because that creates a fresh save on every release. Put the schema version inside the stored document and keep the key stable across compatible updates.
Prevent mixed-version deployments
Hashed asset filenames solve most cache problems: when game.js changes, publish it as a new filename and let the old file remain available long enough for existing sessions to finish. The HTML entry point should be revalidated frequently, while immutable hashed assets can be cached for a long time. What you want to avoid is an old HTML shell loading a mixture of new scripts and removed assets, or a new shell requesting files that a service worker still answers from an older cache.
- Generate content-hashed filenames for scripts, styles, atlases, and other build artifacts.
- Do not delete the previous build's assets at the exact moment the new build goes live; allow a short overlap for open sessions and cached HTML.
- Version service-worker caches and delete obsolete caches only after the new worker activates successfully.
- Make the game detect a build mismatch and request a clean reload instead of continuing with an unknown combination of files.
Test the upgrade path, not only a new game
A clean browser profile proves only that a new player can start. Before publishing, keep representative saves from every schema still in the wild: early game, late game, empty inventory, full inventory, partially completed quests, and any state created by a past bug. Load each fixture in the release candidate and verify both the migrated data and the next save written by the new build.
- Open an old save, play through one meaningful state change, reload, and confirm that the new state persists.
- Test with storage disabled or full so the game fails visibly instead of pretending a save succeeded.
- Open two tabs and confirm that a stale tab cannot overwrite newer progress without warning.
- Run the deployed build once with the browser offline if offline play is supported; this catches incomplete service-worker caches quickly.
Plan the rollback before release
A code rollback is only safe if the previous build can read saves already written by the new one. Often it cannot. For risky schema changes, make the new build read old data but delay writing the new format until the release has proved stable, or keep the previous code capable of recognizing the newer version and refusing to overwrite it. Log migration failures with the build and save versions — never the player's raw save contents — so you can measure the problem without collecting unnecessary personal data.
The practical goal is not an elaborate migration framework. It is a boring update: old players return, their progress loads, the new build replaces the old one cleanly, and nobody notices that a migration happened. Pair this process with the pre-submission playtesting checklist and load-time guide before uploading a replacement build.




