How to Reliably Send Data When Users Close a Page: sendBeacon vs fetch keepalive
This article explains why traditional asynchronous requests often fail during page unload, and demonstrates two modern browser APIs—navigator.sendBeacon and fetch with keepalive:true—that reliably transmit small data without blocking the user experience.
We often encounter a classic scenario: a user is about to close a page or browser tab, and we need to seize that last moment to send important information to the server.
Although it sounds simple, the requirement is challenging in practice. Traditional asynchronous requests such as fetch or XMLHttpRequest are likely to be interrupted by the browser during page unload events, causing the request to fail.
Root cause: why regular requests fail
When a user closes a tab, the browser triggers a series of unload events, such as pagehide and unload. Any standard asynchronous fetch or XMLHttpRequest initiated inside an unload handler faces the problem that the request is sent just as the page is being destroyed, so the browser has no obligation to complete it and will cancel it.
In the past, developers used a synchronous XMLHttpRequest to force the request to block the main thread until completion. While this “works”, it freezes the UI and destroys the user experience.
How can we reliably send this “last letter” without breaking the user experience?
Modern solution 1: navigator.sendBeacon()
navigator.sendBeacon()is a W3C‑designed API whose core mission is to send a small amount of data to the server asynchronously and non‑blocking.
How it works
When sendBeacon() is called, the browser adds the request to an internal queue and returns immediately, without blocking page unload. The browser guarantees that the request will be sent at an appropriate time (e.g., in the background), even if the originating page has already closed.
Features
High reliability : the browser guarantees delivery, unaffected by page unload.
Asynchronous, non‑blocking : does not slow down or affect the user’s closing action.
Simple to use : a very intuitive API.
Data limits : only supports one‑way POST requests and cannot customize request headers.
Code example
Send an analytics log with the time the user spent on the page when they leave:
// Recommended to use the 'pagehide' event, which is more reliable than 'unload'
window.addEventListener('pagehide', (event) => {
// event.persisted === true means the page entered the back‑forward cache and was not actually unloaded
// In that case we usually do not send a beacon
if (event.persisted) {
return;
}
const analyticsData = {
timeOnPage: Math.round(performance.now()),
lastAction: 'close_tab',
};
// Convert data to a Blob, which sendBeacon supports
const blob = new Blob([JSON.stringify(analyticsData)], {
type: 'application/json; charset=UTF-8',
});
// Use sendBeacon to send the data
// Returns true if the data was successfully queued, false if it was too large or malformed
const success = navigator.sendBeacon('/log-analytics', blob);
if (success) {
console.log('Analytics log successfully queued for sending.');
} else {
console.error('Failed to send analytics log.');
}
});Modern solution 2: fetch() with keepalive: true
The fetch API, the cornerstone of modern network requests, also offers an elegant solution. By setting keepalive: true in the fetch init object, we tell the browser that the request is important and should continue after the page unload.
How it works
fetch({ keepalive: true })works similarly to sendBeacon. It marks the request as a “persistent activity”, allowing its lifetime to outlive the current page. The browser processes it in the background just like a beacon request.
Features
High flexibility : unlike sendBeacon, it supports more HTTP methods (e.g., POST, PUT) and allows limited custom headers.
Unified API : if a project already uses fetch extensively, adding keepalive keeps the code style consistent.
Cannot read response : because the page is closed, the front‑end cannot read or handle the server’s response, just like with sendBeacon.
Code example
Automatically save a draft from a text editor when the user closes the page:
window.addEventListener('pagehide', (event) => {
if (event.persisted) {
return;
}
const draftContent = document.getElementById('editor').value;
if (!draftContent) return;
const draftData = {
content: draftContent,
timestamp: Date.now(),
};
// Use fetch with keepalive: true
// Note: even if the request succeeds, the .then/.catch callbacks may not run because the page is unloading
try {
fetch('/api/drafts/save', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(draftData),
keepalive: true,
});
console.log('Draft save request submitted.');
} catch (e) {
// This catch block is unlikely to capture network errors during unload
console.error('Error submitting draft save request:', e);
}
});How to choose?
If the need is to send simple analytics or log data, navigator.sendBeacon() is the most direct and semantically appropriate choice. If greater flexibility is required—such as using PUT to update a resource or setting specific request headers—then fetch({ keepalive: true }) is the better option.
Both modern APIs let us ensure critical data reaches the server without sacrificing user experience.
JavaScript
Provides JavaScript enthusiasts with tutorials and experience sharing on web front‑end technologies, including JavaScript, Node.js, Deno, Vue.js, React, Angular, HTML5, CSS3, and more.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
