Your fetch() in beforeunload is being silently dropped. Use navigator.sendBeacon().
Browsers cancel in-flight network requests when a page unloads. The common fix — synchronous XHR — is deprecated. `navigator.sendBeacon()` is the correct, fire-and-forget API designed exactly for this case.

When a user closes a tab, submits a form, or clicks an external link, you often need to send one last piece of data — a session duration, the last scroll position, an unhandled error report, an A/B test completion event. The natural instinct is to fetch() inside a beforeunload handler. The problem: modern browsers cancel in-flight requests the moment the page begins to unload. Your data never arrives, and you never find out.
The browser's job during a page unload is to navigate away as fast as possible. Keeping a page alive to wait for a network response directly conflicts with that goal. Modern browsers — Chrome, Firefox, Safari — cancel async requests that are in-flight during unload. The beforeunload handler runs, fetch() is called, and the request is silently aborted before it reaches the server.
The old workaround was a synchronous XMLHttpRequest, which blocks the page from closing until the request completes. That approach worked — and also made every tab close feel sluggish. Browsers deprecated synchronous XHR in unload contexts because it reliably degraded user experience. Chrome has been logging warnings about it since 2019.
navigator.sendBeacon() is built for exactly this case:
The browser queues the request and delivers it asynchronously, even after the page has been discarded. The tab can close, the browser can background-suspend the tab, the user can navigate away — the beacon is still delivered. You get no response object back; sendBeacon returns true if the data was successfully queued, false if the payload is too large or the browser rejected it. There is no callback, no .then(), no await. That's intentional: the call is fire-and-forget by design.
beforeunload has a reliability problem beyond network requests: on mobile, it often doesn't fire at all. When the OS suspends a browser tab or the user swipes the app away, there's no beforeunload event — the page just disappears.
document.visibilitychange with document.visibilityState === 'hidden' is more reliable:
The hidden event fires whenever the page leaves the foreground — including on mobile when the user switches apps. It's not a perfect signal for "tab is closing specifically," but it's the closest reliable approximation, and it fires in situations where beforeunload is completely absent.
sendBeacon accepts a BodyInit — the same types that fetch accepts as a body:
The Blob approach with an explicit type is the most useful — it lets you send JSON while controlling the Content-Type header so your server receives it correctly. Without the Blob wrapper, a stringified JSON payload arrives as text/plain, and any middleware expecting application/json will reject or misparse it.
If you need a response from the server, or you want to add custom headers, fetch with keepalive: true is the modern alternative:
keepalive: true tells the browser to keep the request alive even if the page is discarded. Unlike sendBeacon, you can set headers and use any HTTP method. The trade-off: total keepalive payload per page is capped at 64 KB across all requests. sendBeacon has the same limit. For analytics payloads, 64 KB is effectively unlimited — but for large error dumps, be aware of it.
Use sendBeacon when you don't need custom headers and want the simplest possible fire-and-forget. Use keepalive fetch when you need headers, a specific HTTP method, or want to handle a response.
navigator.sendBeacon() is Baseline 2022: Chrome 39 (2014), Firefox 31 (2014), Safari 11.1 (2018). It has been available in every supported browser for years and works in Web Workers. There is nothing to polyfill for any currently-maintained target.
Runs right in your browser — poke at it and watch the concept react live.
Think it clicked? Take the 8-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
Search your codebase for fetch or XMLHttpRequest inside beforeunload or unload handlers. If you find them, the data they send is being silently dropped in a meaningful percentage of page exits. Replace them with navigator.sendBeacon() on visibilitychange, or a keepalive fetch if you need headers. The API is one line, the delivery is reliable, and the browser handles the timing without blocking navigation.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
- 💼 LinkedIn — linkedin.com/in/parsa-jiravand
- ✉️ Email (work & contract inquiries): bestpractice2026@gmail.com
Keep reading
Originally published on dev.to