Recording a game that was never designed to be recorded
There are two ways to build a replay system and only one of them is available here.
The first is to record inputs and re-simulate. It is how a fighting game or an RTS stores a match in a few kilobytes: keep the commands, replay them through the same deterministic engine, and the match reconstructs itself exactly. Roblox offers no deterministic simulation contract — no promise that the same inputs on the same physics produce the same result twice — so this is not merely hard here, it is unavailable.
The second is to record state. Sample where everything is, often enough that the gaps can be smoothed over, and store the samples. That is what this does, and every design decision after it follows from one constraint: the only persistent storage on Roblox is DataStore, and a DataStore key holds 4 MB.
Four reductions, multiplied
Nothing here is a clever single trick. It is four ordinary ones applied in order, and the reason the result is large is that they compose.
Subsampling. The physics tick runs at 60 Hz; recording runs at 10. A timer accumulator on the tick decides when a sample is due, so the recorder never runs on its own clock and never drifts against the simulation. The saving is the obvious one:
before anything else has happened.
Delta encoding. Each entity’s last written state is retained. If the
current sample is identical, that entity is omitted from the frame entirely —
not written as zeroes, not written at all. A
player standing still, dead, or
spectating contributes nothing. A per-entity bitmask records which fields
actually changed, so a player who rotated but did not move pays for the
rotation only.
This is where the difference between a demo viewer and a video recorder shows up. A video costs the same whether or not anything happens. This costs proportional to how much the match moved.
Quantisation. Positions are stored at a tuned precision rather than as full floats, roughly halving the position bytes. The precision is chosen against the thing being rendered — a semi-transparent ghost, seen at a distance, being watched to answer “did that player see this one through a wall” rather than “was that a headshot by two pixels”.
Rotation packing. Rotations compress about four to one, trading a negligible fraction of angular precision for the same reason.
Put together, the payload relative to a naive full-rate recording is roughly
which measures out end to end at 3–10% — the 90–97% reduction, arrived at by multiplication rather than by any one stage being remarkable.
The binary payload is then encoded into ASCII, because DataStore stores strings, and split across keys with a chunk count at the primary key if it is still too big. Reassembly on read is transparent; nothing above the storage layer knows whether it was one key or six.
What that actually costs
The interesting number is not the percentage. It is what the percentage means once it lands in a real DataStore.
A five-minute round with a moderate player count lands in the hundreds of kilobytes — seven to ten per cent of a single key. One real entry pulled back out of the DataStore measured 6 104 bytes, because the delta encoder had almost nothing to write: few entities, little movement, and the stages above compound hardest exactly when a match is quiet.
Server cost is a lightweight accumulator check per physics tick, and at the
sample rate, one transform encode and one comparison per tracked entity. The
hot path is annotated for native compilation. Memory is a rolling window:
which for a 30-second window over twenty entities stays well under a megabyte including Lua table overhead — small enough that the buffer is not a consideration.
Decoding is linear in payload length and in frame count, plus a sort, because frames do not necessarily arrive in write order. For typical replay lengths it finishes in under 100 ms.
Where the fidelity goes
Playback interpolates between samples with smoothstep rather than a straight lerp, which matters more than it sounds: linear interpolation between position samples produces visible direction changes at every keyframe, and a ghost that snaps every tenth of a second reads as broken rather than as approximate.
The error is bounded by how far an entity travels between samples:
At a walking pace of 16 studs per second that is 1.6 studs between samples, and the deviation between the real path and the smoothed one stays well under a stud — imperceptible on a semi-transparent ghost. It grows linearly with speed, so vehicles and teleport abilities are where the approximation shows. That is the honest limit of the whole approach: this is evidence for a moderator, not a frame-accurate spectator feed.
Which is also why the sample rate is not the thing to raise. Doubling it doubles the payload to halve an error nobody watching a ghost can see.
Why not do it the way CS2 does
Counter-Strike 2 records demos by tapping the server’s own outbound network
stream into a file. The Source engine already delta-compresses entity updates
in its netcode, so recording is close to free — but the file is a complete
reconstruction of the match at a 64 to 128 Hz tick, weapons and grenades and
physics props included, and it runs 400 MB to 1.1 GB per match and needs the
game
client to open.
Valorant shipped replays in September 2025 on a deterministic 128-tick lockstep architecture, and had to build a separate state-recording layer because the network layer, designed for competitive integrity, does not emit replay-compatible data. Riot have been open that retrofitting this was a significant engineering problem — which is the general case, not a Riot problem. Adding replay to an engine that already exists is much harder than building it alongside one.
Both routes need something Roblox does not offer: a network layer you can tap, or a simulation you can trust to repeat. Roblox servers are also single-threaded, so a recorder that costs real CPU costs it out of the game loop everyone is playing in. State snapshots at a low sample rate with aggressive compression is not a compromise chosen for convenience — it is the only one of the three that fits the platform.
The trade is per-tick fidelity for server headroom, which is the correct way round when the output is moderation evidence.
The parts that are not compression
Two of them, and both are the sort of thing that decides whether a system gets used after the first week.
Nothing is configured by editing code. Every tunable — sample rate, window length, UI colours, icons, sound ids, localised strings, group-rank permissions — lives in one constants file that a package update does not overwrite. Maps discover themselves through CollectionService tags, and an NPC joins a recording by being tagged, not by being registered somewhere.
The browser is a player feature, not just a staff tool. /report writes
the last thirty seconds to its own clip and tags the accused, which is the
moderation path. But the same browser, on F4, lets anyone search and watch
their own rounds back — and a game where
players can clip and review their own
matches gets something out of the system on the days nobody is cheating.





