How Session Replay Works Under the Hood
Session replay is not screen recording. It captures the structure of a page and every change to it, then rebuilds the page later. Learn how snapshots, mutations, batching, and playback work, and the bugs we hit building ours.
Short answer: session replay records the page's DOM as data, not pixels. It takes one full snapshot of the document, then records each later change (nodes added, attributes changed, text edited) along with input events such as clicks and scrolls, each with a timestamp. Playback rebuilds the snapshot inside a sandboxed frame and applies the changes in order. That is why recordings are small, sharp at any size, and searchable.
Table of Contents
- Not a Video
- Step 1: The Full Snapshot
- Step 2: Incremental Events
- Step 3: Getting Data Off the Page
- Step 4: Storage
- Step 5: Playback
- Bugs We Hit
- Key Takeaways
Not a Video
| Video capture | DOM-based replay | |
|---|---|---|
| Captures | Pixels | Structure and changes |
| Size | Large | Small |
| Sharpness | Fixed resolution | Any size, always crisp |
| Privacy | Hard to redact after the fact | Fields masked before capture |
| Searchable | No | Yes: every click and element is data |
| Needs permission prompt | Yes, in browsers | No |
DeepSync's SDK is built on rrweb, a widely used open-source library for this approach (SDK docs).
Step 1: The Full Snapshot
When recording starts, the recorder walks the entire document and serialises it: every element, attribute, and text node, plus the styles needed to render it. Each node is given a numeric ID. That ID is the key to everything that follows.
The snapshot is the largest single message in a session. Everything after it is a small description of what changed.
Masking happens here. Text inputs, passwords, and payment fields have their values replaced during serialisation, so the real characters are never part of the data (PII masking).
Step 2: Incremental Events
After the snapshot, the recorder listens.
- DOM mutations, through the browser's
MutationObserver: nodes added or removed, attributes changed, text changed. Each refers to nodes by ID. - Mouse movement and clicks, with coordinates and the target node's ID.
- Scrolling, for the window and scrollable elements.
- Input changes, with values masked.
- Viewport resizes.
- Stylesheet changes, including rules added by scripts.
Every event carries a timestamp. In a single-page app the recorder also notes route changes by patching the History API, so client-side navigation appears as page views.
A mutation event is tiny: "node 482 gained child 913, a div with this class". That is all playback needs, provided it already knows about node 482.
Step 3: Getting Data Off the Page
Sending each event as it occurs would mean hundreds of requests. So events are batched. In DeepSync:
- Events flush every 500 milliseconds or once 10 are queued, whichever comes first.
- A full snapshot forces an immediate flush, because replay cannot render without it.
- On page unload, the final batch goes out with
navigator.sendBeacon, which the browser delivers even as the page closes. - Flushing pauses while the tab is idle, to save the visitor's bandwidth.
Step 4: Storage
On the server, replay data is chunked and compressed, and kept until the project's retention period ends, after which it is purged and cannot be recovered (session replay docs).
Alongside the raw events, the server derives structured records: clicks, page views, errors, and frustration signals. Those power heatmaps, funnels, and filters. The replay stream is the evidence; the derived records are the index.
Step 5: Playback
Playback reverses the process.
- Create a sandboxed frame.
- Rebuild the document from the snapshot, restoring each node with its original ID.
- Apply incremental events in timestamp order, on a clock.
- Draw a cursor and click markers from the mouse events.
Because it is a real document, you can pause and inspect it. Scripts from the original page do not run, which is why a recording is safe to open: nothing in it can submit a form or call an API.
DeepSync's player adds click and scroll markers on the scrubber, a timeline of meaningful events, the console and network timeline, and Highlights that jump to rage clicks, errors, and dead ends.
Bugs We Hit
The model is simple. The edge cases are not. Three that taught us something:
A gap in the stream breaks everything after it
Our SDK pauses recording after a period with no interaction, to avoid sending data for an abandoned tab. While paused, events were discarded. The underlying recorder, however, kept observing the page and assigning IDs to new nodes.
When the visitor returned, recording resumed mid-stream. The next mutation referred to node IDs created during the pause, which the player had never been told about. Playback logged "node not found" and stayed broken for the rest of the session.
The fix was to take a fresh full snapshot on resume. A new snapshot re-establishes every ID, so nothing after it depends on the missing stretch. The general rule: an incremental stream cannot tolerate holes. After any gap, start again from a snapshot.
Entrance animations left pages blank
Some recordings showed only a background, with the navigation and content missing. The capture was complete. The cause was CSS entrance animations: elements start at zero opacity and animate in. The recorder captured the starting state, and the player pauses animations so it can control time, which left those elements invisible forever.
The fix was a style rule injected at playback that forces such elements to their finished state.
Images came out the wrong size
Inlining images into the recording dropped the width and height attributes on optimised image elements, so they replayed at the bitmap's natural proportions. We switched to referencing images at their original address, which preserves the attributes. The trade-off is that an image must still exist at that address when the recording is watched.
What This Design Cannot Do
Because replay reconstructs the DOM, content outside the DOM is difficult: canvas drawing, cross-origin frames, and closed shadow roots. We cover those in why session replay struggles with iframes, canvas, and shadow DOM.
Key Takeaways
- Replay records structure and changes, not pixels.
- A full snapshot assigns IDs; later events refer to them.
- Events are batched and compressed.
- Any gap in the stream must be followed by a new snapshot.
Conclusion
Session replay works because a web page is already a structured document. Record the structure and its changes faithfully, and you can rebuild any moment of a visit.
Frequently Asked Questions
Related articles
Stay in the loop
Get the latest insights on product analytics and user behavior delivered to your inbox.



