Flukz — open-source shmup community resource open-source GPL devlog  ·  about
// indie shmup & game-dev resource

Engineering · July 6, 2026

Save File Integrity: Handling Corruption and Crash-Safe Writes in Indie Games

Players will forgive a bug. They will not forgive losing a save file, especially one representing hours of progress on a punishing shmup run. Corruption is rare, but when it happens it happens at the worst possible moment.

The naive save implementation opens a file, writes the serialized game state directly to it, and closes it. This works in every test session because tests rarely crash, lose power, or get force-quit mid-write. In the real world, a save can be interrupted at any byte offset — a laptop battery dies, a player alt-F4s during autosave, the OS kills the process for memory pressure — and if the write was in progress, the result is a half-written file that is neither the old save nor the new one. Loading it either crashes or silently loads garbage state, and either way the player's progress is gone.

Write to a temporary file, then rename

The standard fix, borrowed from how databases handle this same problem, is to never write directly to the live save file. Write the new save data to a temporary file first, flush it fully to disk, and only then rename the temp file over the original. A file rename on the same volume is atomic at the filesystem level on every major platform — it either completes entirely or doesn't happen at all, with no possibility of a half-renamed file. If the process dies mid-write, the temp file is corrupted but the original save is untouched; if it dies mid-rename, the filesystem guarantees you still get one or the other, never a mix.

This pattern is documented in detail in general systems literature — the POSIX rename() semantics that make this guarantee are described in the Linux man-pages project's rename(2) reference, and the same atomic-rename approach underlies how most embedded databases, including SQLite's rollback journal, guarantee crash consistency.

Checksums catch the corruption that atomic writes don't

Atomic rename protects against a save being caught mid-write, but it doesn't protect against bit rot on a failing drive, a buggy cloud-sync client overwriting the file with a stale copy, or a player manually editing a save file and breaking its structure. A checksum (even something as simple as a CRC32 over the serialized data, stored alongside it) lets the load routine detect a corrupted file before trying to parse it, rather than crashing partway through deserialization with a confusing stack trace.

When a checksum mismatch is detected, the worst response is a hard crash with no explanation. The better response is a clear message telling the player their save appears corrupted, with an option to load the previous backup if one exists, or start fresh if it doesn't. Silent failure — loading a corrupted save as if it were valid and letting weird bugs cascade from there — is the outcome to avoid above all else, since it turns a save problem into a much harder-to-diagnose gameplay bug report days later.

Keep at least one backup generation

The cheapest insurance against all of this is keeping the previous save generation around instead of overwriting it outright. Rotate: write new save, verify its checksum, then only after that succeeds rename the previous save to a ".bak" suffix and promote the new one. If a save is ever found corrupted on load, falling back to the ".bak" copy loses at most one session's progress instead of the entire save file — a meaningfully different experience for the player, and a much easier support conversation.

Test corruption deliberately, rather than waiting for a bug report

Crash-safe saving is one of the few systems where you can and should test the actual failure mode directly rather than just trusting the design on paper. A simple test harness that kills the process at a randomized point during a save write, across a few hundred repeated runs, will surface bugs in the atomic-write logic far faster than waiting for it to happen organically during normal development, where interrupted saves are rare by definition. This kind of fault-injection testing feels like overkill for a solo project until the first time it catches a real bug — a missing flush call, a rename that wasn't actually atomic on a particular filesystem, a race between the save thread and an autosave timer — that would otherwise have shipped invisibly and only shown up as a player's lost save months later.

It's worth running this kind of test against every platform you ship on, not just the primary development machine. Filesystem behavior around atomic rename and write durability is not perfectly uniform across Windows, macOS, Linux, and console filesystems, and a save system that's provably crash-safe on one platform can have a subtly different failure mode on another if the underlying filesystem guarantees differ.

This connects directly to the sync problem covered in cloud saves and cross-platform sync for indie games — a corrupted local save that syncs to the cloud before anyone notices can overwrite a good remote copy, so the same atomic-write-plus-checksum discipline needs to extend to whatever sync layer sits on top of local storage. It's also worth reading alongside save states and continue systems, since continue data and full save data often share the same underlying write path and need the same crash-safety guarantees.