Minimal reproduction of a memory leak in the @sentry/browser XHR instrumentation.
Every XMLHttpRequest stays reachable for the life of the page when an app opens
the next request from the readystatechange callback of the previous one.
hls.js does this for Low-Latency HLS playlist reloads. This page does the same
without hls.js.
node server.js
Open http://localhost:8787/?sentry=on in Chrome and press Start. The page runs four request chains. Then open http://localhost:8787/?sentry=off in a second tab as the control.
- In each tab, open DevTools, Memory panel, and take a heap snapshot after 30 seconds.
- Filter the class list by
XMLHttpRequestand note the count. - Wait 2 minutes and take a second snapshot.
Expected with sentry=on: the XMLHttpRequest count equals the number of
completed requests, and the heap grows with every request.
Expected with sentry=off: the count stays at 4, and the heap stays flat.
instrumentXHR in @sentry-internal/browser-utils creates const virtualError = new Error()
in the open() wrapper and never releases it. The setRequestHeader Proxy that the
wrapper assigns on the XHR instance shares a closure context with virtualError, so
the Error stays reachable from the XHR. The Error holds raw V8 stack frames, and each
frame holds its receiver. When request N+1 is opened from the callback of request N,
the receiver of one frame is request N. The result is a linked list of every request.
The in-flight request is a Blink pending activity, so it is a GC root. The list hangs from it.
| Query parameter | Default | Meaning |
|---|---|---|
sentry |
on |
off loads no Sentry bundle. |
chains |
4 |
Parallel request chains. |
size |
8192 |
Response body size in bytes. |
delay |
0 |
Server delay per request in ms. 500 is close to an LL-HLS playlist rate. |
version |
10.73.0 |
Sentry browser bundle version from browser.sentry-cdn.com. |
autostart |
false |
true starts without a click. verify.js uses it. |
verify.js drives Chromium with playwright-core, runs both modes, and prints the live
XMLHttpRequest count and heap size. It needs playwright-core with a Chromium install.
PLAYWRIGHT_CORE=/path/to/node_modules/playwright-core node verify.js 40
Result on macOS, Chromium 1200 (Playwright 1.57), Sentry browser bundle 10.73.0, 40 seconds, no server delay:
sentry=on: completed 224991 requests. Live XMLHttpRequest: 15591 -> 229113. Heap: 28.2 MB -> 400.7 MB
sentry=off: completed 242321 requests. Live XMLHttpRequest: 4 -> 8. Heap: 0.7 MB -> 0.7 MB