BrowserGateway

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=true are 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/replays

Defaults: 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
FieldDefaultWhat it controls
retentionDays7Recordings older than this are deleted by the cleanup sweep. 0 keeps them forever.
maxBytesPerSession500 MBSafety cap. Capture stops for a session that exceeds this. Minimum 1 MB.
filesystem.path./replaysWhere recordings are stored, relative to $BG_DATA_DIR.
capture.formatjpegjpeg is much smaller on disk. Switch to png for lossless frames.
capture.quality70JPEG quality (1-100). Ignored for PNG.
capture.everyNthFrame1Frame 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 counts

A 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

EndpointMethodPurpose
/v1/replaysGETList recordings, newest first. Accepts ?since=<iso>&limit=<n> (default 100, max 500). The response also echoes the active replay: settings.
/v1/replays/:idGETOne recording's metadata, with a per-target summary (frame count, size, first and last URL).
/v1/replays/:idDELETEPermanent delete.
/v1/replays/:id/manifestGETThe full frame manifest as JSON. Returns 404 until the session has closed.
/v1/replays/:id/frames/:frameGETOne frame as an image, for example /frames/12.jpeg. Frame numbers run across the whole session. Long-cached.
/v1/replays/:id/parts/:partGETOne 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 marked truncated: "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 droppedFrames in complete.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 typeRecorded?
WS /v1/connect?session_record=true (Puppeteer, Playwright, any CDP client)Yes
WS /v1/connect without session_recordNo
WS /v1/live (Playground page)No
POST /v1/screenshot / /v1/content / /v1/scrapeNo

Cross-references

  • Profiles and recording. Both work on the same session. When ?profile=<id> is used, the recording's metadata includes the profileId.
  • Provider pinning and recording. A session pinned with ?provider=<id> still records. The recording's providerId reflects 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.

On this page