JavaScript Beacon API Table

PieceWhat it doesField note
navigator.sendBeacon(url, data)The fire-and-forgetqueued by the browser process, survives page death; returns boolean queued - never delivered
the unload problemThe reason it existsfetch in unload/pagehide races teardown and loses (mobile may not even fire unload) - the beacon outlives the page
POST-only contractThe shapealways POST, no custom headers, no response readable - validation happens elsewhere
payload typesThe allowed bodiesstring/Blob/FormData/URLSearchParams/ArrayBuffer - Content-Type inferred; cross-origin Blob types can hit CORS walls
visibilitychange: hiddenThe right momentthe reliable last call on mobile AND desktop; unload is the event that never fires on phones
the 64KB budgetThe queue capper-origin IN-FLIGHT total, not per beacon - batch many events; overflow silently returns false, so check it
delivery semanticsThe best effortno retry, no callback, duplicates possible - idempotent event ids server-side make retries harmless
fetch keepaliveThe escalated siblingsame ~64KB budget, but custom headers/auth/methods allowed; big payloads = IndexedDB persist + replay next load
Reference: the MDN Beacon API. A one-way wire that outlives the page: the browser completes the POST after your document is gone - the only send that does, and the reason session-end analytics actually arrive.
Bottom line: the transport is easy, the timing design is the work. Fire cumulative state on visibilitychange-to-hidden (mobile never fires unload), check the boolean and persist-and-retry when refused, batch events to respect the 64KB in-flight cap, and treat every event as idempotent - beacons are best-effort with possible duplicates, so the server keeps the last state and unique ids collapse the noise.
Related tools: the fetch table (the keepalive escalation path), the visibility table (the hidden event that times the send), the speculation rules table (prerendered pages that must NOT count beacons), the performance timing table (the numbers the beacons carry), and the IndexedDB table (persist-and-retry for refused batches).

The Beacon API solves one precise problem: sending data at the exact moment the page dies. A normal fetch started in an unload or pagehide handler gets cancelled when the document goes away - your analytics event, form draft save, or session-end ping silently never lands. navigator.sendBeacon(url, data) hands the request to the browser: it is queued and completed by the browser process after the page is gone, no response expected, nothing to await.

Bottom line: a beacon is a one-way wire with a budget. It is always POST, carries no custom headers, returns only a boolean meaning queued (not delivered), and ignores you forever after. The payload budget is about 64KB of in-flight data per origin - not per beacon - so batch events into fewer, larger beacons, and when the queue is full sendBeacon quietly returns false (check it, then fall back to fetch with keepalive or persist to IndexedDB and retry next load).

The modern timing rule beats the API itself: fire on visibilitychange to hidden, not on unload. Mobile browsers freeze or discard tabs without ever firing unload - the event you were taught to use is the one that never runs on phones. And because delivery is best-effort with possible duplicates, the receiving end should treat every beacon as an idempotent event: unique event ids server-side, and no counting logic that a retry can corrupt.

How to use

  1. Fire at the right moment: document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') navigator.sendBeacon('/log', payload); }); - hidden is the reliable last call on mobile and desktop; keep a pagehide handler as belt-and-suspenders.
  2. Mind the types: sendBeacon accepts string (text/plain), Blob, FormData, URLSearchParams and ArrayBuffer/TypedArray - the browser infers Content-Type from the object, and cross-origin Blobs with non-safelisted types can be refused by CORS rules before the page even dies.
  3. Check the boolean: const ok = navigator.sendBeacon(url, data); if (!ok) queueForNextLoad(data); - false means the queue refused you (budget, invalid type); the fallback is IndexedDB persistence and a replay on next visit.
  4. Batch by budget: the 64KB cap is per-origin in-flight, so coalesce - accumulate events in an array and send one JSON beacon every N events or on hidden, instead of a beacon per click that starves the queue at session end.
  5. Need headers or methods? Use fetch with keepalive: true - same 64KB budget, but you can set Authorization, use PUT, read CORS mode; use sendBeacon when you want the smallest possible code and no ceremony.

Frequently asked questions

Why do my analytics events disappear exactly when users leave?

Because the document that started the request ceases to exist before the request finishes. A fetch in an unload handler races the page teardown: mobile browsers usually win the race (they may not even fire unload - the tab is frozen or discarded outright), desktop browsers cancel in-flight requests on navigation. sendBeacon hands the request to the browser process, which owns it after your page is gone - that hand-off is the entire API. The complementary mistake is timing: many teams call it on unload, which mobile never fires. The pattern that actually collects data everywhere: send incrementally during the session, and on visibilitychange-to-hidden send the final cumulative state as one beacon. Session-end data you can rely on is a timing design, with sendBeacon as the transport.

What does the boolean return value actually tell me?

Only that the browser accepted the request into its outliving queue - not that it was delivered, and never whether the server liked it. Three practical consequences. First, false means refused: the per-origin in-flight budget (~64KB) is exhausted or the payload type is not allowed - the correct response is persist-and-retry, not silent loss. Second, true does not mean delivered: the network can still fail, and there is no retry, no callback, no promise - fire-and-forget is the contract, so event payloads should be idempotent (a unique event id server-side makes accidental duplicates harmless). Third, there is no response: if the endpoint redirects or returns an error, your page will never know - validate the endpoint separately during development, not per-event.

How do I send auth headers or larger payloads than a beacon allows?

Two escalation paths with the same budget. fetch(url, {method: 'POST', keepalive: true, headers: {Authorization: token}, body}) shares the ~64KB in-flight cap but adds everything fetch can do: custom headers, content types without Blob/CORS friction, PUT/PATCH semantics, credentials mode. It is the right tool when the receiving endpoint is an API that requires authentication. For genuinely large data, neither works - the honest pattern is hybrid: persist the payload to IndexedDB, send a small beacon saying 'batch id 123 waiting', and replay the full payload on the next page load (or from the service worker, which has no unload problem at all). Analytics vendors converged on exactly this: beacons for the final state summary, storage-plus-replay for the heavy detail.

Does sendBeacon work everywhere, and how does it interact with bfcache?

Support is universal in current engines (Chrome, Firefox, Safari, Edge since roughly 2016-2017) - this is one of the safest APIs to use without feature detection, though a typeof guard costs one line. The subtler interaction is the back-forward cache: a page restored from bfcache was never unloaded, so a beacon you sent on visibilitychange-hidden already fired, and the pageshow event with persisted: true marks the return - your analytics should start a NEW session segment there rather than assume the old one continued. Also note the timing asymmetry: because hidden fires when switching tabs (not just leaving), a naive hidden-handler can send many 'session end' beacons per actual session - the cumulative-state-plus-idempotent-id design absorbs this: each beacon carries the full state, the server keeps the last, duplicates collapse harmlessly.

Related tools