Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

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.

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 shape

A 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.

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.

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.

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 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.

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.