Skip to content

WebSocket event streaming ​

Use GET /recordings/{recordingId}/events/ws for an advanced custom transport that streams rrweb events over one WebSocket connection. The Browser Client already owns this transport, buffering, and HTTP fallback for most integrations. If you maintain your own transport, start with the reliability limits in Migrate from rrweb.

Should I use the packer or WebSocket streaming? ​

They work at different layers, so the choice depends on which backend you run.

The packer compresses individual events so they take less space wherever you store them. It is a library feature described under optimize storage. WebSocket streaming is the transport that delivers events to rrweb Cloud as they happen. The Browser Client adds an HTTP fallback. A custom transport must implement its own. With your own backend you choose whether to pack. With rrweb Cloud, transport is handled for you.

Authenticate the WebSocket connection ​

Browser WebSocket connections cannot set a custom Authorization header during the upgrade. Put a browser-safe public write key in the token query parameter instead:

ts
const recordingId = crypto.randomUUID();
const publicWriteKey = 'public_key_rr_your_key';
const url = new URL(
  `wss://api.rrweb.com/recordings/${encodeURIComponent(recordingId)}/events/ws`,
);
url.searchParams.set('token', publicWriteKey);
url.searchParams.set('contentType', 'application/x-ndjson');

const socket = new WebSocket(url);

Never put a private API key in browser code or a WebSocket URL. The public key in the URL is still a credential: WebSocket URLs can appear in proxy logs, monitoring, telemetry, and error reports. Redact the token query parameter and avoid logging the complete URL.

The optional query parameters are:

  • contentType: application/x-ndjson, application/ndjson, or application/json; the default is application/x-ndjson.
  • contentEncoding: gzip, br, or zstd; omit it for identity encoding.
  • debug=true: echoes decoded payload data and forwarding details. Use it only with privacy-safe test recordings because those frames contain captured data.

Send rrweb events over the WebSocket ​

Let record() create event objects. rrweb supplies the numeric event type, millisecond timestamp, and event-specific data; do not invent string event types or placeholder snapshots.

ts
import { record } from '@rrweb/record';

let stopRecording: (() => void) | undefined;
const retainedEvents: unknown[] = [];

socket.addEventListener('open', () => {
  stopRecording = record({
    blockSelector: '[data-private]',
    maskTextSelector: '[data-mask]',
    maskAllInputs: true,
    emit(event) {
      if (socket.readyState !== WebSocket.OPEN) {
        retainedEvents.push(event);
        stopRecording?.();
        return;
      }

      const frame = `${JSON.stringify(event)}\n`;
      socket.send(frame);
    },
  });
});

function finishRecording() {
  stopRecording?.();
  socket.close(1000, 'capture complete');
}

retainedEvents only demonstrates that a failed send must not discard an event. It is memory-only and is not a production retry queue; persist the queue and its acknowledged position before capture if page termination or a crash must not lose it.

Text frames are appropriate for uncompressed NDJSON. Use binary frames only when the payload is encoded according to contentEncoding. Measure encoded bytes with TextEncoder before sending; the ingest route rejects an individual frame at or above 1 MB.

Confirm events were accepted ​

The server starts an upstream flush after roughly 256 KiB is buffered or after about one second of inactivity. Closing the socket requests a final flush. A final successful result has this shape:

json
{
  "type": "upstream-result",
  "ok": true,
  "status": 204,
  "statusText": "",
  "headers": {},
  "final": true
}

Routine successful non-final flushes do not produce a message. Failures, quarantined rows, debug output, and the final flush can produce messages. A normal final result closes with code 1000; an upstream failure closes with 1011.

These result messages are not a per-event or application-level acknowledgement. WebSocket.send() only hands a frame to the browser, and a disconnect can leave the client unable to tell which in-flight events Cloud accepted. Retrying uncertain frames can duplicate or reorder events; dropping them can break the snapshot chain.

Before production, design durable queuing, sequence checkpoints, acknowledgement, resume, and retry behavior. If those guarantees are not part of your application, use the Browser Client or the serialized HTTP transport in Migrate from rrweb.