Session Replays
Record what the browser rendered during a session and read the frames back later through the REST API.
Record what the browser rendered during a session, frame by frame, and read the frames back later through the REST API.
On Browser Gateway Cloud, recordings are also encoded to MP4 and kept for 30 days. See Recordings.
When To Use Them
- AI agent debugging. Your agent failed mid-workflow. Pull the frames around the failure and see what the page actually showed.
- Customer support. A user reports "the bot didn't see the popup." Pull the session's frames and confirm.
- Audit. Regulated industries need proof of what a headless browser actually rendered for a given session.
- Provider comparison. Record the same job on Provider A and Provider B. See where they diverge visually.
How It Works
Recording is chosen per session. When a client connects with ?session_record=true, the gateway adds a screencast capture step to that session's connection. Chrome sends an image each time the rendered page changes, and the gateway writes those images to disk as they arrive. When the session ends, the gateway writes a manifest that lists every frame and a completion record.
A few things worth knowing:
- Capture rides the same connection your client already uses. The gateway does not open a second connection to the provider, and nothing runs inside the page being recorded.
- Every page and iframe the session opens is recorded, including popups.
- Recording sets each page's viewport to 1280 x 720 before it starts. Chromium does not produce frames without a viewport set first (puppeteer#10527). If your script depends on a different viewport, set it again after the page opens.
- Sessions without
session_record=trueare not recorded and pay no capture cost.
Quickstart
There is no switch to turn on in gateway.yml. Add ?session_record=true to the connect URL:
const browser = await puppeteer.connect({
browserWSEndpoint:
"ws://localhost:9500/v1/connect?session_record=true&token=YOUR_BG_TOKEN",
});When the session closes, list recordings with the REST API:
curl -H "Authorization: Bearer $BG_TOKEN" http://localhost:9500/v1/replaysDefaults: JPEG frames at quality 70, 7-day retention, 500 MB cap per session.
Configuration Reference
The replay: block is optional. It controls how recordings are stored, not whether they happen.
replay:
retentionDays: 7 # delete completed replays older than this. 0 = keep forever
maxBytesPerSession: 524288000 # 500 MB cap per session
filesystem:
path: ./replays # resolved under $BG_DATA_DIR
capture:
format: jpeg # jpeg (smaller, default) or png (lossless, larger)
quality: 70 # 1-100, only used when format=jpeg
everyNthFrame: 1 # 1 = every visual change; raise to skip frames| Field | Default | What it controls |
|---|---|---|
retentionDays | 7 | Recordings older than this are deleted by the cleanup sweep. 0 keeps them forever. |
maxBytesPerSession | 500 MB | Safety cap. Capture stops for a session that exceeds this. Minimum 1 MB. |
filesystem.path | ./replays | Where recordings are stored, relative to $BG_DATA_DIR. |
capture.format | jpeg | jpeg is much smaller on disk. Switch to png for lossless frames. |
capture.quality | 70 | JPEG quality (1-100). Ignored for PNG. |
capture.everyNthFrame | 1 | Frame sampling. 1 captures every visual change. |
Older configs that contain replay.enabled still load. The key is ignored.
Storage Layout
$BG_DATA_DIR/replays/
<session-id>/
meta.json written when capture starts (provider, profile, start time, format)
parts/
000.bin frames packed back to back, each with a 4-byte length prefix
001.bin
...
manifest.json written on session close: one record per frame
complete.json written on session close: end time, frame count, size, dropped and skipped countsA new part file starts every 25 MB or every 5 minutes, whichever comes first. Each manifest record gives the frame number, timestamp, page URL, viewport size, scroll offset, target ID, and where its bytes sit in the part files.
A recording with no complete.json is still in progress, or its session ended without a clean close (for example, the gateway stopped mid-session).
Provider Support
Recording works with any provider that supports the Chrome DevTools screencast command. If a provider does not support it, the gateway logs a warning and the session runs normally with no frames recorded.
REST API
| Endpoint | Method | Purpose |
|---|---|---|
/v1/replays | GET | List recordings, newest first. Accepts ?since=<iso>&limit=<n> (default 100, max 500). The response also echoes the active replay: settings. |
/v1/replays/:id | GET | One recording's metadata, with a per-target summary (frame count, size, first and last URL). |
/v1/replays/:id | DELETE | Permanent delete. |
/v1/replays/:id/manifest | GET | The full frame manifest as JSON. Returns 404 until the session has closed. |
/v1/replays/:id/frames/:frame | GET | One frame as an image, for example /frames/12.jpeg. Frame numbers run across the whole session. Long-cached. |
/v1/replays/:id/parts/:part | GET | One raw part file, for example /parts/000.bin. Long-cached. |
All endpoints use the standard BG_TOKEN auth. :id is the session ID, the same one shown on the Sessions page and in GET /v1/sessions.
To play a recording back, read the manifest, then fetch frames in order and show each one for the gap between its timestamp and the next.
Retention
A cleanup sweep runs every 6 hours. It deletes any recording whose session ended more than retentionDays ago. Recordings that never finished (no complete.json) get an extra 24 hours before they become eligible.
Set retentionDays: 0 to keep recordings forever. You are responsible for disk space at that point.
Safety Caps
- Per-session byte cap (
maxBytesPerSession, default 500 MB) protects against a runaway tab filling the disk. When a session hits the cap, capture stops for that session, a warning is logged, and the frames already captured are kept. The manifest is markedtruncated: "byte-cap". - Bounded write queue. If frames arrive faster than the disk can write them, extra frames are dropped rather than queued without limit. The count appears as
droppedFramesincomplete.json. - Your automation is not affected. Capture never blocks or alters the commands your client sends.
Frame Deduplication
Each frame is compared with the previous frame from the same page. Frames with identical bytes are skipped, so only one copy lands on disk. A frame is only skipped when it matches exactly, so no visual change is lost. The skip count appears as duplicatesSkipped in complete.json.
What Gets Recorded
| Connection type | Recorded? |
|---|---|
WS /v1/connect?session_record=true (Puppeteer, Playwright, any CDP client) | Yes |
WS /v1/connect without session_record | No |
WS /v1/live (Playground page) | No |
POST /v1/screenshot / /v1/content / /v1/scrape | No |
Cross-references
- Profiles and recording. Both work on the same session. When
?profile=<id>is used, the recording's metadata includes theprofileId. - Provider pinning and recording. A session pinned with
?provider=<id>still records. The recording'sproviderIdreflects the pinned provider. - Live view and recording. Both use Chrome's screencast but serve different purposes. Live view streams to the dashboard in real time and saves nothing. Recording saves to disk for later.
Limitations
- The self-hosted dashboard has no replay player. Use the REST API to list, read and delete recordings.
- No video export. Frames are stored as individual JPEG or PNG images.
- No PII redaction. If you record sessions handling sensitive data, set a short retention and restrict access to the gateway.
- Recordings are stored on local disk. Multi-instance deployments need a shared mount if every instance should see every recording.
Troubleshooting
No recording appears. Confirm the connect URL has session_record=true exactly (not 1 or yes). Check the gateway log for a screencast arm failed warning, which means the provider rejected the screencast command.
The manifest returns 404. The session is still open, or it ended without a clean close. The manifest is written when the session closes.
Disk fills up. Lower retentionDays, set a smaller maxBytesPerSession, or raise capture.everyNthFrame to 2 or 3 to sample fewer frames.
Dropped frames count is high. Disk write speed is the bottleneck. Move $BG_DATA_DIR to faster storage, or keep capture.format on jpeg to cut bytes per frame.