What a replay keeps
A development build keeps each page’s play as a replay. This page says what it keeps and what that costs.
What it keeps, and where
Section titled “What it keeps, and where”The dev server and the dev room write each game’s replays under its .replays folder, which the game’s .gitignore leaves out, a session a folder:
.replays/ 20260927-183940-19mw/ a session: one run of the room, named as the run session.json when it began and ended, its room's port and process 20260927-183945-hqxe/ a page that played in it replay.json how it began, its marks, each piece's bytes, its clock's slips released.json the marks an agent released pieces/000003/ the session's fourth minute records.bin.zst the room's frames, what the page sent, each frame's length, the input and the keyframes; records.bin while the page still writes to it events.jsonl its log, an event a line shots.bin its screenshots, one file for the minute start.json the stream's world as it begins, where the minute before was deleted rooms/20260927-183940-19mw/ the room's own recording room.json the scene, when it began, its port and process, and its settings checkpoint-base.json.zst its first checkpoint, and its physics then: physics-base.bin.zst what each later one is compressed against pieces/000003/ the same minute of the wall, opening on a checkpoint actions.jsonl.zst every action, a line each; actions.jsonl while open checkpoint-10800.json.zst the whole world before step 10801, and one physics-10800.bin.zst each 600 steps after it, with Rapier's snapshot 20260927-190210-qq4w/ a page that played with no room: its own session saved/boss-fight/ a saved clip, in a session's shapeA session is one run of the room, from its start to a restart, with every page that played in it. The dev server files each page under the session of the room its address names, and moves it to the new session when the room restarts, so a page that played across a restart is in both, on its one clock. A game that plays alone, or a page whose room keeps no recording, has a session for each page load.
The session’s clock is split into pieces of a minute of the wall: a page’s piece of a minute and the room’s piece of the same minute share a number, and the trim deletes them together. The room checkpoints as each minute begins and every 600 steps, ten seconds, inside it, so a continue catches up at most ten seconds. Its steps may fall behind the wall, where the machine slept with the room running; its minutes do not, and a reader finds a step by the checkpoints each minute opens on. A page’s clock stops while the machine sleeps in Chromium on macOS, Linux and Android, and runs on in Chromium on Windows. Where it stopped, the page writes a line to its log that says how far the wall ran ahead, and the dev server files its later records by the wall’s minute, so they still go with the room’s minute of the same time. A room that restarts in its process on a change to the scene closes its run first, and the run’s session.json says when.
The whole folder but the saved clips, every session’s pages, room and shots, fits one budget by size alone, 300 MB unless the game sets another. There is no age limit: a creator back from a week away finds everything. As a page plays, at most every five seconds, the dev server measures the sessions, and while they pass the budget it deletes the oldest minute of any session that no mark protects, oldest first on the wall’s clock; with no page open, nothing trims. A long session shrinks from its start a minute at a time, and the short session a quick restart leaves costs almost nothing and stays. Before a page’s minute goes, its world is folded into the start.json of the minute that follows it on the page’s own clock, the next minute or the first after the minutes a sleep skipped, so what remains still plays back and continues. The minute a session is writing now stays, and a session the trim leaves with nothing, whose room and page have stopped, goes whole. A 31-minute Holdfast session played by the bot took 56.2 MB, about 1.8 MB a minute: 1.7 MB of it the page’s and 0.15 MB the room’s. A person’s play takes about 1.3 MB a minute, so 300 MB holds about three to four hours.
What is binary, and so read by no search anyway, is compressed with zstd: a page’s records once the page is into the next minute, a room’s actions once the room is past their minute, and each checkpoint as it is written. The minute a page or a room stopped in is compressed by the next trim, once the page has posted nothing for two minutes or the room has stopped. The log and each file an agent edits stay plain text. Most of a checkpoint is what the scene built and rarely changes, the ground’s scatter and the physics world’s heightfield and obstacles, so each is compressed with the run’s first as zstd’s dictionary: on Holdfast a checkpoint takes about 8 kB where alone it takes about 360 kB.
A page with no input for two minutes keeps nothing until the next one, a dev server with no page open keeps nothing, a room records only while a player is in it, and ?replay=off in the address keeps nothing for that page. Each page’s recording and each room’s run names the shape it was recorded in, and one another version of the engine recorded refuses to play, continue or re-run, in one sentence: record the moment again.
Playback and a continue need a room: a game that plays alone keeps its log, its shots and its input, and play and rerun refuse it.
Settings
Section titled “Settings”Every limit is a setting of the game’s config, through the engine’s Vite plugin, spawnite():
plugins: [ spawnite({ replay: { budgetMegabytes: 300, markShare: 0.5, savedWarningMegabytes: 200, shotIntervalSeconds: 2, shotWidth: 854, }, }),],| Setting | Default | What it sets |
|---|---|---|
budgetMegabytes |
300 | The megabytes every session takes together, pages, rooms and shots; past it the trim runs. |
markShare |
0.5 | The share of the budget the minutes marks protect may fill; past it a new mark protects none. |
savedWarningMegabytes |
200 | The megabytes of saved clips past which the tools warn. |
shotIntervalSeconds |
2 | The seconds between the rolling shots; 0 takes none. |
shotWidth |
854 | The rolling shot’s width in pixels, at the canvas’s shape. |
A recorded video clip, from the dev tools’ Record button, saves its window of the replay as saved/clip-<name>, and its own two limits sit beside replay, as the devtools page says:
| Setting | Default | What it sets |
|---|---|---|
clips.maxSeconds |
30 | The seconds after which a recording stops by itself. |
clips.keep |
50 | The clips the game keeps; past it, Record is refused, and none is deleted. |
A setting the plugin does not know, or a value out of its range, fails the config’s load and names it. The cli reads the same settings from the game’s config, so spawnite replay list measures against the game’s own budget. For one page load, the address changes the shots alone, as ?replay=off turns the page’s replay off, so a profile prices a value without an edit:
| Parameter | Sets | Example |
|---|---|---|
?replay-shot-interval= |
shotIntervalSeconds |
?replay-shot-interval=0 |
?replay-shot-width= |
shotWidth |
?replay-shot-width=640 |
A value that does not read is a warning in the page’s log, and the game’s setting stands. spawnite play profile --query replay-shot-width=640 passes one.
Dev builds only
Section titled “Dev builds only”The engine’s Vite plugin defines __GAME_ENGINE_DEV__ as true in development alone, and every piece of the replay, the recorder, F8, the menu’s line and the Settings row, sits behind it, so a production build never has the chunk. The shots’ settings, __GAME_ENGINE_REPLAY__, are defined in development alone. games/holdfast/test/build.test.ts builds Holdfast for production and fails on any module of the replay’s, or its words, in the output. A room records only where ROOM_REPLAY=1 says so, which the cli sets on each game’s room it starts unless ROOM_REPLAY=0 is set, and a production room’s image never does; the replication protocol between the page and the room is the same either way.
What it costs
Section titled “What it costs”Measured on Holdfast’s dev page at 1920 by 1080 with vsync at 100 Hz, played by spawnite play profile --drive bot for 60 s in a fresh room, three runs with the replay on and three with ?replay=off, alternately, on a Ryzen 9800X3D and an RTX 4070 SUPER. Frames are counted from a second after the loading screen lifted:
| Each run’s reading | Replay on, three runs | Replay off, three runs |
|---|---|---|
| Mean frame | 9.61, 9.56, 9.63 ms | 9.57, 9.55, 9.53 ms |
| p99 frame | 10.1 ms each | 10.1 ms each |
| Frames over 20 ms | 7, 6, 5 | 5, 4, 5 |
| Frames over 50 ms | 2, 1, 0 | 0, 0, 0 |
| Main thread’s p99 | 6.5, 6.3, 6.4 ms | 6.5, 6.2, 6.3 ms |
| Shader programs linked after the lift | 70 each | 70 each |
The three frames over 50 ms, of 56, 70 and 56 ms, came in the second after the lift, where the voice’s first connection runs, and the profile’s sampler named no function of the replay’s in them. A screenshot costs the main thread about 0.1 ms, and at most 0.4 ms, at any width: the shrink is the GPU’s and a worker reads it back, compares it and encodes it, where the same read on the main thread held it 3 ms. The replay kept 2.2 MB a minute of that play, 1.4 MB of records and 0.8 MB of shots, and about 1.3 MB a minute of a person’s, before its records were compressed; they now take about 0.2 MB a minute of it.
The shots’ size
Section titled “The shots’ size”The rolling shot’s width is a trade between what an agent can tell from it, the image tokens it spends to look, the bytes the replay keeps and the frame. Measured on Holdfast’s dev page at 1920 by 1080 with the display at 115 Hz, played by the bot for 60 s with F8 at 30 s, three runs at each width and three with no rolling shots, alternately, on the machine above. The bytes are the rolling shots’ own, with every shot kept; the frame’s readings are the medians spawnite play profile compare gives after the lift, and the main thread’s time a shot is read round the page’s calls at the page’s 0.1 ms resolution:
| Rolling shot | Image tokens | A shot | Shots a minute | Main thread a shot | Frame mean, p99 after the lift |
|---|---|---|---|---|---|
| 640 by 360 | about 300 | 27 kB | 0.84 MB | 0.1 ms, 0.2 ms max | 8.7 ms, 8.5 ms |
| 854 by 480 | about 550 | 36 kB | 1.08 MB | 0.1 ms, 0.4 ms max | 8.7 ms, 8.5 ms |
| 1280 by 720 | about 1,230 | 62 kB | 1.90 MB | 0.1 ms, 0.2 ms max | 8.7 ms, 8.5 ms |
| none | none | none | none | none | 8.7 ms, 8.5 ms |
compare found nothing worse past the machine’s noise at 640 or 1280; at 854 it marked the frame in the half second after the lift, 75 to 83 ms against 0 to 67 ms, which the timeline puts on the voice’s first connection, not the replay. A mark’s shot at 1920 by 1080 takes 190 to 225 kB.
The same moment read at each width: at 640, an agent names the character, her pose and her weapon, the rune stones and the coins, but not the rune on a stone or what a small thing far down the path is; at 854 both read; 1280 adds detail no question about the moment needed. So the rolling shot is 854 by 480, a quarter more bytes than 640 for a view an agent can read far into, and the mark, the moment a person chose, keeps every pixel. None of the five engines keeps shots in a replay: Unreal’s and CS2’s replays are the stream alone, and Unity’s Recorder writes a video at a size you pick. A crash reporter such as Sentry’s attaches one screenshot at the screen’s size at an error, as a mark does here.
The room’s recording costs the page nothing it can measure. Three runs of the same profile with the room’s recording on and three with it off read a frame mean of 8.6 ms each, a p99 of 8.5 ms each, and 2 or 3 hitches over 50 ms after the lift either way; spawnite play profile compare found nothing worse past the machine’s noise.
The room pays for each checkpoint on its own thread, in the step that takes it, once each 600 steps and as each minute begins. Measured on Holdfast’s room played for 62 s by a Node client that runs, shoots and takes the monsters’ blows, three runs with ROOM_REPLAY=1 and three with 0, alternately; spawnite play room --seconds 62, with --room-env ROOM_REPLAY=0 for the runs with it off and --room-env ROOM_HEARTBEAT_SECONDS=15 for the same windows, now plays and reads such a run:
| Each 15 s heartbeat’s reading, from the second on | Recording on | Recording off |
|---|---|---|
| Mean step | 0.78 to 0.95 ms | 0.80 to 0.87 ms |
| Longest step | 5.5 to 7.1 ms, once 10.4 ms | 1.5 to 2.6 ms |
| Longest checkpoint | 5.4 to 6.7 ms, once 8.9 ms | none |
A checkpoint takes the world, about 2 ms, the room’s own state, and its JSON, about 1.5 ms, most of both the ground’s scatter; the zstd and the write run off the room’s thread. Writing the physics world back from its snapshot, as the room does with each checkpoint so a continue from it comes back exactly, adds a restore of that snapshot to the step that takes it. Two bots played each game for 40 s with spawnite play room --bots 2: the longest checkpoint read 8.0 to 10.3 ms on Holdfast over three runs, against 8.8 ms in one without it, and 9.6 to 18.3 ms on the arena over four runs, against 9.2 and 10.9 ms in two without it. A step’s budget is 16.7 ms, and the room’s clock runs a late step’s time on the next. Compressing a closed ten seconds of the page’s records took the dev server about 0.5 ms, and made them 8 times smaller; a closed minute takes about six times that.
A continue runs as fast as the room steps: Holdfast’s 69 s run took 0.7 s to check, and an 8 s moment 0.3 s, past the 5 s the command takes to load the game’s room.
Opening a moment with spawnite replay play or shot --render took 12 to 14 s from the command to its first shot: the command’s own dev server answers in 0.6 s, and the page loads the game and stands on its keyframe 6.7 to 8.3 s after it opens. Each later shot takes 0.5 to 2 s, most of it the frames played to reach it.
Other engines
Section titled “Other engines”CS2’s demos, Fortnite’s replays and Unreal’s demo recording keep the server’s replication stream, not a video, and play it through the client, as playback does; Unreal also keeps a checkpoint every 30 seconds to jump to. Unreal’s in-memory replay keeps a rolling buffer by time, and NVIDIA’s instant replay and OBS’s replay buffer keep the newest minutes by time or by size; each drops a recording whole or from its oldest end. Here one size budget covers every session, a mark protects its window without a copy, and a clip an agent saves is the one copy. StarCraft 2 and Factorio replay the players’ actions into a lockstep simulation from its start, and Factorio proves its saves whole by loading one and setting what it steps beside the game, which is what check does. This keeps a replay of every dev playtest without a line of the game’s code, and the room’s own checkpoints and actions beside it, so an agent continues any moment of it exactly with the code as it stands, and the room’s record, not a tolerance, says it came back.
Unreal’s replays pause, change speed and scrub, jump back through their checkpoints, and fly a spectator camera or follow any actor; a live replay lets a spectator rewind a match still running and jump back to live, as a room session’s seek and seek live do. Fortnite’s and Rocket League’s replay theaters give players the same controls. Godot’s game view suspends a running game, steps it a frame at a time and swaps in the editor’s free camera, and Roblox Studio pauses a playtest and frees its camera. Unity’s Recorder shoots from any camera over a play mode paused in the editor. Each is a person’s panel. Here the same controls are commands, so an agent holds a moment, moves its camera and shoots it with no script of its own.