Ren'Py rollback and saves: store vs persistent and checkpoint control
A practical breakdown of Ren'Py's store vs persistent data, when to use checkpoint() and block_rollback(), and how to verify rollback behavior in your own project.
13 Sept 2025, 11:32 UTC

The rollback trap in Ren'Py
When a player presses page-up or clicks the rollback mouse wheel, Ren'Py rewinds the game state to the most recent automatic snapshot. For most variables, this happens silently and as expected. But the engine's snapshot mechanism is not universal, and assumptions about what gets saved or restored can lead to lost progress or broken saves.
Store vs. persistent: the data that rolls back and the data that doesn't
Ren'Py automatically snapshots the store at the start of most statements. This snapshot powers both the page-up rollback and the quick-save system. Any variable you assign to store — such as store.current_scene or store.flags — is included in that snapshot. When the player rolls back, those variables revert to their snapshot values.
In contrast, the persistent object is designed to survive saves, loads, and even the start of a new game. It is the right place for unlock flags, gallery progress, settings menus, and anything you want the player to keep across sessions.
The key distinction is that store is rollback‑safe but session‑scoped, while persistent survives across sessions but is not automatically rolled back by page-up. Choosing the right container for each piece of game state is the first engineering decision.
When automatic checkpoints aren't enough
Ren'Py inserts automatic checkpoints at statement boundaries, after menus, and at label transitions. But long Python blocks, dynamically composed screens, or code that runs between statement boundaries may not get a snapshot until the next natural checkpoint. If a player rolls back past a critical moment, side effects from that block can be lost or duplicated.
The engine provides renpy.checkpoint() to force a rollback point at a chosen moment. This is useful after a Python block that sets multiple store variables, composes a scene, or triggers audio cues you want to preserve across a single rollback step.
Similarly, renpy.block_rollback() can be used as a label argument or context manager to declare that a given block is a rollback boundary. Any code inside is treated as a unit: either the whole block rolls back, or none of it does. This is important after irreversible events like timed choices, point-of-no-return scenes, or anything where a partial rollback would leave the game in an inconsistent state.
Concrete pattern: tracking gallery unlocks across rollback
# In an early label or script block
persistent.gallery = [] # survives new games, loads, saves
# When an image is shown, record the unlock
label show_image:
$ persistent.gallery.append("image_a")
renpy.checkpoint() # forces a rollback point after the append
show "image_a"
# If the player later rolls back past this label,
# the checkpoint ensures the append is either fully undone or fully preserved,
# depending on where the rollback boundary sits.
The checkpoint() call ensures that if the player presses page-up after this point, the gallery list reverts consistently with the rest of the snapshot, rather than leaving a stale append that outlives the dialogue state.
Limitations and how to verify them
- Mutating a list or dict stored in
storeinside a Python block can leak changes across rollback boundaries because rollback restores variable bindings, not always deep‑copied mutable contents. Test this on your installed Ren'Py version. - Behavior around
saystatements, menus, and screens has changed across Ren'Py 7.x and 8.x. If you are upgrading a project, run the rollback verification steps after the update. - Overusing
block_rollback()frustrates players who expect to re‑read text. Reserve it for genuinely irreversible moments.
To check your own setup, create a minimal test project:
- Set a
storevariable, roll back with page-up, and confirm the value reverts. - Set a
persistentfield, load the save, and confirm it survives a restart. - Place a mutable list in
store, append inside a Python block, roll back, and inspect whether the append persisted — this confirms deep‑mutation behavior on your version. - Call
renpy.checkpoint()after a scene and verify the rollback log skips it; consult the Ren'Py documentation section on rollback for your installed version.
Closing: engineer your rollback boundaries
Rollback and save behavior in Ren'Py is powerful once you know what the engine snapshots and what it doesn't. Use store for temporary, rollback‑friendly state, persistent for lasting progress, and insert renpy.checkpoint() or renpy.block_rollback() when the automatic snapshot pattern doesn't match your game's logic. Test the four verification steps above on your project, and you'll avoid the most common save‑rollback surprises.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.