Skip to content

Session replay never records: upload uses fetch keepalive:true and hits the browser's 64 KB cap #7

Description

@DLO33

Summary

@rybbit/js uploads session-replay batches with fetch(..., { keepalive: true }). The Fetch standard caps the total body of keepalive requests at 64 KiB; when exceeded, fetch() rejects with TypeError: Failed to fetch and the request never leaves the browser.

An rrweb FullSnapshot (initial DOM, with inlined stylesheets) is almost always far larger than 64 KB, so the very first/most important replay batch is rejected client-side. The SDK re-queues the failed batch and retries it, so the same oversized snapshot is attempted forever and nothing behind it in the queue ever sends. Net result: session replay silently never records even though the SDK initialised fine and rrweb is recording.

/api/track and /api/identify are unaffected because their bodies are tiny (well under 64 KB).

Environment

  • @rybbit/js 0.6.2 (latest), bundled with rrweb 2.0.1 via Vite
  • Self-hosted Rybbit backend (reproduces regardless of backend version — the request never reaches the server)
  • Chromium-family browser (Edge 149); applies to any browser implementing the keepalive 64 KB limit (all current Chrome/Edge/Firefox/Safari)

What you observe

  • Every session shows has_replay: 0; no replay is ever stored.
  • DevTools → Network (filter session-replay): a flood of POST /api/session-replay/record/<siteId> entries, each with an empty response, repeating rapidly (the retry loop).
  • Server access logs show zero of these POSTs arriving — they fail before leaving the browser.
  • /track and /identify work normally.

Root cause

In dist/index.mjs / dist/index.js, the replay batch upload:

await fetch(`${analyticsHost}/session-replay/record/${siteId}`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body,
  mode: "cors",
  keepalive: true,   // ← bodies > 64 KB are rejected by the browser with TypeError
});

Per the Fetch spec, the sum of in-flight keepalive request bodies may not exceed 64 KiB, otherwise the request is a network error. keepalive is intended for small beacons that must survive page unload — it's the wrong tool for replay batches, which are (a) often large and (b) sent continuously during the session, not at unload.

The failed batch is then unshifted back onto the queue and retried indefinitely, so the oversized snapshot blocks the whole pipeline.

Reproduction

  1. Init the SDK on any reasonably complex page so the FullSnapshot exceeds ~64 KB (e.g. any app with a real stylesheet).
  2. Enable session replay.
  3. Observe in DevTools that POST .../session-replay/record/... requests fail with TypeError: Failed to fetch and repeat; no replay appears in the dashboard.

Suggested fix

Don't use keepalive for replay batch uploads. Either:

  • Drop it entirely for /session-replay/record (replay batches are sent mid-session; they don't need unload-survival), or
  • Use it conditionally: only set keepalive: true when the serialized body is under a safe threshold (e.g. < 60 KB), otherwise send a normal fetch. If an unload-time flush is needed, split that path so only the small tail uses keepalive (or use navigator.sendBeacon, which has the same 64 KB limit, with a size guard).

/track and /identify can keep keepalive: true (they're small and benefit from unload survival).

Workaround (for self-hosters in the meantime)

Patch the one fetch call to keepalive: false via patch-package:

# after editing node_modules/@rybbit/js/dist/{index.mjs,index.js}:
#   body:<x>,mode:"cors",keepalive:!0   →   keepalive:!1   (replay upload only)
npx patch-package @rybbit/js

This fully restores session replay (verified: replays record and play back correctly).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions