Skip to main content

Session replay failures

Session replay recordings are captured in the visitor’s browser, uploaded in batches, and stored against the conversation so an agent can play them back later. When the Session Replay tab in the Inbox misbehaves, the cause is one of a small set: the overflow offload failed (503), the stored recording drifted out of shape (corrupt), the capture never reached the server, or the recording was purged or capped. This runbook walks each symptom. The storage and lifecycle model behind it is documented in the session replay concept page; request and response shapes are in the Session Replay API reference.

Reproduce the failure

  1. Load the conversation in the Inbox and open the Session Replay tab.
  2. Note exactly which symptom you see:
    • Empty — no recording is attached to the conversation at all.
    • Spinner forever — a recording exists but the player never receives a playable event stream.
    • Corrupt notice — the player starts and then errors or truncates mid-stream.
  3. Read the recording back with the session replay read endpoint (see the API reference) and note status (recording / ended / capped), offloaded, truncated, total_event_count, and blob_chunks. These fields tell you which tier of storage the events live in and which section below applies.
Match the symptom against the sections that follow before escalating.

SESSION_REPLAY_OFFLOAD_FAILED (503)

Symptom: the widget’s capture call returns 503 with SESSION_REPLAY_OFFLOAD_FAILED (see error codes), and playback shows fewer events than the session ran for. A recording stores its first 5,000 events (up to 4 MB serialized) inline on the conversation row — the inline head. Everything past the head spills to durable object storage as one chunk per upload batch, addressed session-replay/{schema}/{recording_id}/chunk-{seq}.json. The 503 means that spill path failed for a batch: the platform could not persist the batch’s overflow events to object storage. What this does and does not mean:
  • The capture endpoint asks the widget to retry the batch, and the chunk write is idempotent on (recording_id, seq) — a retry rewrites the same chunk rather than duplicating events. While the visitor’s page is still open, retries usually heal the gap on their own.
  • The inline head is still served. Playback of the first 5,000 events / 4 MB keeps working; only events past the head are affected.
  • Events past the head are not recoverable until a later batch retry succeeds. If the visitor’s page closed before the retry landed, that batch’s events never left the browser and are gone — there is no server-side backfill for events the server never received.
What to do: if the gap is recent and the visitor is still on the page, let the widget retry. Persistent 503s across many conversations point at object-storage availability rather than one bad recording; treat that as platform status, not per-conversation repair. The recording itself remains playable at the head in the meantime.

Playback corrupts / truncates mid-stream

Two distinct failure shapes look similar in the player; tell them apart before acting. Truncated playback on an offloaded recording. If a chunk download fails during reassembly, the read endpoint degrades instead of erroring: it serves the inline head only, with offloaded: true on the recording. Retrying the read usually returns the full stream. Persistent head-only playback on an offloaded recording is worth a support ticket with the chunk seq numbers (below). Corrupt recording. Every read path narrows the decoded metadata->'session_replay' value through a shape guard before using it; a drifted or malformed stored shape fails that guard loudly instead of being played from or appended to. The observable contract is:
  • The operator read endpoint returns session_replay: null rather than the corrupt payload — the tab shows empty even though a value exists in metadata.
  • The visitor append path returns 500 CORRUPT_SESSION_REPLAY and stops the recorder, so a corrupt recording never grows.
  • The delete (purge) endpoint is shape-tolerant: a right-to-erasure request still erases the recording even when the shape is unreadable, so erasure is never blocked by corruption.
What to do: a corrupt recording cannot be repaired in place — purge it with the delete endpoint and let the next visitor session capture a fresh recording. If the purge is for an erasure request, it completes regardless of the corruption.

Sensitive fields appear in playback

Masking is decided at capture time, in the visitor’s browser — masked content never leaves the page, and the server stores exactly what the browser sent and hands it back unchanged at playback. Masking is therefore never retroactive: relaxing or tightening the policy today does not alter recordings already stored. The default posture is fail-closed (DEFAULT_SESSION_REPLAY_PRIVACY): mask_all_inputs: true and mask_pii_default: true, with empty mask_text_selectors and block_selectors. Every <input>, <textarea>, and <select> value is masked, and the recorder’s built-in PII heuristics apply, until you explicitly relax them. If sensitive values appear in playback, the policy in force when that session was captured allowed them — typically a relaxed mask_all_inputs, or free-text PII rendered outside an input that no mask_text_selectors / block_selectors entry covered. The controls are tenant-owned: read the current policy and update it with PUT /api/v1/session-replay/privacy (owner/admin/developer role plus conversations:write), using mask_text_selectors for rendered text and block_selectors to cut whole subtrees (payment iframes, regulated content) out of capture entirely. Changes take effect from the next widget bootstrap. For recordings that already carry PII, the remedy is erasure: purge them with the delete endpoint. Record which recordings were purged per your own data-protection process.

Replay is missing on a conversation I expected to have one

Work through the reasons a recording can be absent, in order:
  1. No capture ever started. A recording is created only when a visitor session opts in through the widget capture surface. A conversation created by API, import, or a channel without the widget has no replay by design.
  2. Purged. The delete endpoint removes the recording (durable chunks first, then the metadata pointer), and deleting the conversation removes its attached recording with it — conversation retention and recording retention move together. If your retention or erasure tooling deleted the thread, the replay went with it.
  3. Capped by the abuse backstop. A recording is hard-bounded at roughly 1 GiB of serialized events or 5 million events across both tiers (MAX_REPLAY_TOTAL_BYTES / MAX_REPLAY_TOTAL_EVENTS). Only that backstop flips status to capped and sets truncated: true — the inline head cap never truncates, it only redirects overflow to chunks. A capped recording is a signal about the page (a script in a rapid DOM-mutation loop), not a storage limit to raise; what was stored before the cap still plays back.
  4. Corrupt. A drifted stored shape reads back as session_replay: null (previous section) — the tab shows empty even though metadata holds a value.

Duplicate recordings / two replays on the same conversation

Capture holds a single-active-recording invariant: starting a recording while a non-ended recording exists reuses the existing one instead of creating a second, and an ended recording is immutable. Two recordings can therefore never fork from one live session. If you see what looks like two replays on one conversation, compare recording_id, started_at, and visitor_id: you are looking at separate visitor sessions — a later session started after the earlier recording ended, and the conversation’s replay surface shows the newest recording. That is expected behavior, not a duplicated capture. If the started_at ranges of two recordings genuinely overlap, include both recording_id values and their client-side timestamps when you escalate.

Widget capture endpoint unreachable

The widget buffers rrweb events in the browser and posts them in batches of at most 500 events. While the capture endpoint is unreachable — visitor offline, a corporate proxy or content blocker intercepting the route — events accumulate in the in-browser buffer and nothing is persisted server-side. When connectivity returns, buffered batches upload and the recording fills in; if the page closes first, the buffered events are lost, because they never left the visitor’s machine. Symptom: the conversation exists and the visitor was active, but the replay is missing or noticeably shorter than the session, with no 503s logged server-side (the requests never arrived). Check the visitor’s network path and console for failed capture posts before treating this as a platform defect; server-side storage health is not the suspect when the endpoint was never reached.

Escalation

When the sections above do not resolve the failure, open a support ticket with:
  • conversation_id and recording_id.
  • For offloaded recordings: the seq numbers of the chunks around the gap (from blob_chunks), so the missing range can be located in object storage.
  • Client-side timestamps: when the visitor’s session ran, and when the 503 / corrupt-read / missing-replay symptom was observed.
  • The exact Inbox symptom from “Reproduce the failure” (empty, spinner, corrupt notice) and the recording’s status / offloaded / truncated fields from the read endpoint.
  • For masking issues: the privacy policy in force at capture time.

What NOT to try

  • Do not re-initialize the recorder mid-capture. The start call is idempotent and reuses the active recording, so re-initializing cannot repair a stuck or failing capture — it only produces the same recording. Recovery for a failed batch is the widget’s retry of that batch, which the 503 response already requests.
  • Do not batch-upload past 500 events per call. The capture endpoint enforces a per-batch event cap; an oversized batch is rejected whole rather than partially stored. Split uploads into batches of at most 500 events.
  • Do not hand-edit the session_replay metadata to “repair” a corrupt recording. The purge endpoint is shape-tolerant for exactly this case; erase and recapture instead.

See also