ScreenshotNeo

BlogEngineering

How to Stream Browser Sessions with a Session Replay API

Learn how to stream rrweb session events over WebSockets, replay them live, secure the data, and choose between self-hosted and managed APIs.

By the ScreenshotNeo team1 October 20269 min read

Short answer: stream a browser session as structured replay events, not as a video feed. Record an initial DOM snapshot, emit incremental DOM, pointer, input and scroll events, send them over a WebSocket to an ingestion service, persist them with a session ID, and let a replay player rebuild the page by applying the events in order. Use HTTP as a fallback when the socket is interrupted or the page is unloading.

rrweb provides the event model and recording/player building blocks. Its Browser Client sends rrweb events to rrweb Cloud over WebSocket by default and falls back to HTTP when needed. The same architecture can be implemented with a self-hosted backend such as OpenReplay, or consumed through managed products such as LogRocket and FullStory.

1. How a live session replay stream works

  1. Record in the browser. Start a recorder after you have configured masking, blocked fields, sampling and consent behavior.
  2. Create a recording identity. Give each recording a stable session or recording ID. Keep it available across tabs only when that matches your product’s privacy model.
  3. Send events continuously. Serialize each event as JSON and send it over WebSocket. Queue events while disconnected.
  4. Ingest and store. The backend authenticates the write request, validates event size and ordering, and stores events by recording ID.
  5. Replay. The player loads the initial snapshot and applies incremental events according to their timestamps.

This is different from screen recording. A replay stream represents page state and user actions, so it can be queried, sampled and integrated with error or support systems. It also means that privacy controls must be applied before data leaves the browser.

2. Minimal WebSocket implementation

The following example uses the open-source rrweb recorder in a browser and a small Node.js WebSocket receiver. Your production recorder should use the privacy options and consent flow required by your application.

Install the receiver

mkdir replay-server
cd replay-server
npm init -y
npm install ws

Node.js WebSocket ingestion server

const { WebSocketServer } = require('ws');
const crypto = require('crypto');

const wss = new WebSocketServer({ port: 8080 });
const recordings = new Map();

wss.on('connection', (socket, request) => {
  const url = new URL(request.url, 'http://localhost');
  const recordingId = url.searchParams.get('recording_id');

  if (!recordingId || recordingId.length > 200) {
    socket.close(1008, 'recording_id is required');
    return;
  }

  if (!recordings.has(recordingId)) recordings.set(recordingId, []);

  socket.on('message', (raw) => {
    let event;
    try {
      event = JSON.parse(raw.toString());
    } catch {
      socket.close(1003, 'invalid JSON');
      return;
    }

    if (!event || typeof event !== 'object' || typeof event.type !== 'number') {
      socket.close(1003, 'invalid rrweb event');
      return;
    }

    const record = {
      id: crypto.randomUUID(),
      receivedAt: Date.now(),
      event
    };
    recordings.get(recordingId).push(record);

    // Broadcast to connected live viewers in a real implementation.
    console.log(recordingId, event.type, event.timestamp || null);
  });
});

console.log('Replay ingestion listening on ws://localhost:8080');

Browser recorder

Bundle rrweb in your frontend (for example, with npm install rrweb) and connect only after the user has consented. The exact masking and blocking options depend on the recorder version and your data model.

import { record } from 'rrweb';

const recordingId = crypto.randomUUID();
const socket = new WebSocket(
  `ws://localhost:8080/?recording_id=${encodeURIComponent(recordingId)}`
);

const pending = [];
let open = false;

socket.addEventListener('open', () => {
  open = true;
  for (const event of pending.splice(0)) socket.send(JSON.stringify(event));
});

socket.addEventListener('close', () => {
  open = false;
});

const stop = record({
  emit(event) {
    if (open && socket.readyState === WebSocket.OPEN) {
      socket.send(JSON.stringify(event));
    } else {
      pending.push(event);
    }
  },
  // Configure masking, blocking and sampling here before production use.
});

// Call stop() when the recording should end.

For production, add authentication, a maximum queue size, event batching, backpressure and an HTTP flush path for unload. Never put a private server credential in this browser bundle. rrweb’s hosted model separates a public write-scoped browser key from private API credentials.

3. Sending events with HTTP fallback

WebSocket is appropriate for continuous low-latency capture, but mobile networks, proxies and page unloads can interrupt it. Keep a bounded queue and POST unsent events to a trusted ingestion endpoint when the socket closes. The endpoint should deduplicate by event ID or sequence number.

async function flushOverHttp(recordingId, events) {
  if (!events.length) return;
  const response = await fetch('/replay/ingest', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ recordingId, events })
  });
  if (!response.ok) throw new Error(`HTTP fallback failed: ${response.status}`);
}

Send batches rather than one request per event. Include a monotonically increasing sequence number when your storage layer needs deterministic ordering. On reconnect, resume from the last acknowledged sequence and discard only events confirmed by the server.

4. Building a live replay viewer

A viewer needs the initial snapshot before incremental events. Your backend can stream new events to viewers over another WebSocket channel, or the viewer can poll a recording API. The player must preserve event timestamps so playback speed, pause and seek remain meaningful.

// Conceptual viewer flow
const events = await fetch(`/replay/${recordingId}/events`).then(r => r.json());

// Pass the ordered rrweb events to the rrweb player in your frontend.
// When a live event arrives, append it to the player input or update the
// player according to the player API used by your rrweb version.
viewerSocket.addEventListener('message', (message) => {
  const event = JSON.parse(message.data);
  // validate event, then deliver it to the live player
});

For a current user, integrations can expose a session ID or replay URL. FullStory documents browser session details, replay URLs and server-side listing of recent sessions; those links can be attached to support tickets, traces or error reports.

  • Mask passwords, payment data, health data and other sensitive inputs before recording.
  • Block entire elements or pages that must never enter the event stream.
  • Obtain and record appropriate consent before enabling production capture.
  • Sample sessions when full coverage is unnecessary, and define retention by data category.
  • Use a public write-scoped browser key only for ingestion; keep private read and administration credentials on trusted servers.
  • Issue temporary or presigned access for untrusted viewers instead of exposing permanent replay credentials.
  • Restrict replay URLs, audit sharing, and test cross-origin frames, iframes, WebSockets and page-unload behavior.
  • Validate event size, event type and recording ownership at ingestion.

Legal requirements depend on your geography and the data you capture. Vendor controls do not replace your own retention, consent and access policies.

6. Self-hosted versus managed replay services

Option Best fit Relevant capabilities Trade-offs
rrweb Cloud API-first hosted capture Browser WebSocket ingestion, HTTP fallback, recording and replay APIs, hosted previews, public/private key separation You operate less infrastructure but depend on the hosted service’s controls and retention model
OpenReplay Teams that need an open-source, self-hostable stack Live replay, WebSocket capture, privacy controls, integrations and APIs You own deployment, upgrades, storage, scaling and operations
LogRocket Managed diagnostics Live Mode plus DOM, logs, network, performance and Redux context Managed convenience with the vendor’s product and data model
FullStory Managed replay and session linking Session IDs, replay URLs, custom events and server-side session listing Managed operations and retention are controlled through the service

Compare services on WebSocket and fallback behavior, replay latency, masking and consent controls, storage and retention, API access, replay-link integration, and diagnostic context such as console, network, performance and application state.

7. Reliability and performance considerations

  • Bound memory: cap the in-browser queue and decide whether to drop oldest events or stop capture when the limit is reached.
  • Batch safely: batch by count or time, but flush immediately for page visibility changes and unload.
  • Reconnect deliberately: use exponential backoff with jitter and resume from an acknowledged sequence.
  • Keep ordering: store the initial snapshot before incremental events and reject impossible sequence transitions.
  • Isolate replay work: process compression, persistence and live fan-out off the request path where your runtime permits.
  • Control sampling: sample by user, route or error state so diagnostic sessions are retained without capturing every visit.
  • Measure the pipeline: track connection failures, queue drops, fallback usage, ingestion rejection, storage growth and viewer lag.

Do not claim a fixed latency or storage rate without measuring your own pages. DOM size, mutation frequency, media, browser behavior and sampling rules change the event volume substantially.

8. cURL, Python and Node.js API patterns

Your own replay API will normally expose a write endpoint, a recording-events endpoint and a viewer endpoint. Keep the path names and authentication scheme explicit in your API documentation. The following examples show the shape of requests without inventing a vendor-specific URL.

cURL

curl -X POST "https://replay.example.com/v1/recordings/RECORDING_ID/events" \
  -H "Authorization: Bearer SERVER_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @events.json

Python

import requests

with open("events.json", "rb") as payload:
    response = requests.post(
        "https://replay.example.com/v1/recordings/RECORDING_ID/events",
        headers={
            "Authorization": "Bearer SERVER_TOKEN",
            "Content-Type": "application/json",
        },
        data=payload,
        timeout=30,
    )
response.raise_for_status()

Node.js

import { readFile } from 'node:fs/promises';

const payload = await readFile('events.json');
const response = await fetch(
  'https://replay.example.com/v1/recordings/RECORDING_ID/events',
  {
    method: 'POST',
    headers: {
      Authorization: 'Bearer SERVER_TOKEN',
      'Content-Type': 'application/json'
    },
    body: payload
  }
);
if (!response.ok) throw new Error(`Ingestion failed: ${response.status}`);

9. Troubleshooting

Symptom Likely cause Fix
Viewer is blank The initial snapshot was not stored or events arrived out of order Persist and load the snapshot first; validate sequence and timestamps
Events stop on navigation Socket closed during page unload Flush a bounded queue through HTTP on visibility change and unload
WebSocket connects, then closes Authentication, origin, proxy or message-size limits Inspect close codes and proxy logs; authenticate at connection time and raise limits deliberately
Private values appear in replay Masking or blocking was configured after capture started Apply privacy rules before start() or record(), then delete affected recordings
Duplicate events HTTP fallback retried an unacknowledged batch Attach an event ID or sequence and make ingestion idempotent
High CPU or bandwidth Large DOM mutations, media or an overly high capture rate Sample sessions, block unnecessary content, batch events and profile representative routes
Replay links expose data Permanent public URL or read token Use authorization, short-lived signed access and an audit trail

10. Or skip the browser setup

If you need a clean image or PDF of a page rather than an interactive time-based replay, ScreenshotNeo provides a single GET request. Its API is useful for snapshots in support workflows, visual checks and AI-agent tools; it does not replace event capture for replaying user actions.

See the ScreenshotNeo API documentation for all options. A basic request is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Cookie banners, newsletter popups and chat widgets are removed before the shot. Bot checks, blank pages and failed loads are never billed, and the response identifies the page verdict and billing status in headers. An MCP server lets AI agents use take_screenshot, get_page_info and capture_pdf. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

11. FAQ

Is a session replay stream video?

Usually no. It is a timestamped event stream that reconstructs the DOM and user activity. This makes it searchable and integrable, but it requires a compatible player.

Can I use only WebSockets?

You can, but page unloads and network changes create gaps. A bounded queue plus HTTP fallback is safer for continuous capture.

Where should replay events be stored?

Use storage designed for your retention and access requirements, partitioned by recording ID and ordered by sequence or timestamp. Keep viewer access separate from ingestion credentials.

When should I self-host?

Self-host when deployment ownership, data residency or deep customization outweighs the operational work. Choose a managed service when integrated diagnostics and vendor-operated infrastructure matter more.

Can screenshots replace session replay?

No. A screenshot captures one rendered state. Replay events let you reconstruct a sequence of states and actions. Use screenshots for point-in-time evidence and replay for behavioral debugging.