← Home ← Codex ← DEBT ← Engine
Browse by Category
+ added · updated 7d
← Back to glossary

navigator.sendBeacon

JavaScript ES2015 Intermediate
debt(d7/e3/b3/t7)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7). While an ESLint regex could flag unload+fetch patterns, silent failures (payload >64KB, ignored boolean return, missing beacons on mobile) typically only surface through code review or telemetry gap analysis.

e3 Effort Remediation debt — work required to fix once spotted

Closest to 'simple parameterised fix' (e3). The quick_fix is swapping unload+fetch for navigator.sendBeacon on visibilitychange/pagehide — a small localized refactor of the telemetry flush path, more than one line but confined to one module.

b3 Burden Structural debt — long-term weight of choosing wrong

Closest to 'localised tax' (b3). Applies only to the analytics/telemetry layer of a web app; other components are unaffected by the choice.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t7). Per misconception, developers expect a Promise or delivery confirmation but get a synchronous boolean meaning only 'queued'; combined with silent 64KB limits, no header support, and no CORS error surface, it contradicts how fetch behaves elsewhere.

About DEBT scoring →

Also Known As

sendBeacon beacon API navigator beacon

TL;DR

navigator.sendBeacon queues a small asynchronous POST that survives page unload, designed for analytics and diagnostic pings.

Explanation

navigator.sendBeacon(url, data) schedules an HTTP POST that the browser guarantees to attempt even if the page is being unloaded, hidden, or navigated away from. Unlike fetch, it does not block unload, does not accept custom headers or methods, and returns only a boolean indicating whether the request was successfully queued - not whether it eventually succeeded. This makes it ideal for fire-and-forget telemetry: session analytics, error reports, exit surveys, page-visibility metrics. Payloads count against a shared quota (typically 64 KB across all in-flight beacon and keepalive-fetch requests) and content type is inferred from the data argument (Blob, FormData, URLSearchParams, ArrayBuffer, or string). Because it runs at low priority and cannot be cancelled or awaited, do not use it for anything the application logic depends on. Historically developers tried synchronous XHR in unload handlers to flush analytics; browsers now block that pattern, and sendBeacon is the sanctioned replacement. Pair it with the pagehide or visibilitychange (hidden) events rather than unload, since mobile browsers frequently skip unload entirely - visibilitychange fires reliably when a tab is backgrounded or closed. If you need custom headers (e.g. Authorization), sendBeacon cannot help; use fetch with keepalive: true, which shares the same lifecycle guarantees and the same 64 KB quota but allows full request configuration. Note that CORS applies: cross-origin beacons trigger preflight for non-simple content types, which can silently fail if the server does not respond correctly. Because there is no response callback, debugging failed beacons requires server-side logging or DevTools network inspection with 'preserve log' enabled.

Common Misconception

sendBeacon returns a Promise or confirms delivery - it returns a synchronous boolean indicating only whether the request was queued, with no way to observe the eventual network outcome.

Why It Matters

Analytics and error-reporting pipelines rely on delivering the last events before a user leaves, and misusing fetch or synchronous XHR in unload handlers loses data on mobile browsers or gets blocked by the browser entirely. sendBeacon (or fetch keepalive) is the only reliable way to flush telemetry across page transitions.

Common Mistakes

  • Attaching beacon logic to the unload event, which mobile browsers frequently skip - use visibilitychange with document.visibilityState === 'hidden' or pagehide instead.
  • Trying to set custom headers like Authorization on sendBeacon - it doesn't support them; use fetch with keepalive: true when headers are required.
  • Exceeding the 64 KB payload limit and silently losing beacons because the return value is not checked.
  • Using sendBeacon for critical requests whose success the app depends on - it is fire-and-forget with no response.
  • Ignoring CORS preflight failures on cross-origin beacons because there is no error callback to surface them.

Avoid When

  • The request must succeed and the application logic depends on the response.
  • You need custom headers such as Authorization - use fetch with keepalive: true instead.
  • Payloads exceed the 64 KB beacon quota.
  • You need to observe or retry the request on failure.

When To Use

  • Flushing analytics or session metrics when a page is hidden or closed.
  • Sending crash reports or error diagnostics from window.onerror handlers.
  • Recording last-known page-visibility state on visibilitychange transitions.
  • Emitting small telemetry pings that tolerate occasional loss.

Code Examples

✗ Vulnerable
// Wrong: unload event is unreliable on mobile, and fetch without keepalive is cancelled
window.addEventListener('unload', () => {
    fetch('/analytics', {
        method: 'POST',
        body: JSON.stringify({ event: 'exit', duration: sessionMs })
    }); // often aborted before it leaves the browser
});

// Also wrong: sendBeacon has no way to attach Authorization or other headers
// (the signature is sendBeacon(url, data) — no options object)
navigator.sendBeacon('/analytics', JSON.stringify(payload));
// Auth header silently omitted; server rejects as unauthenticated.
✓ Fixed
// Flush telemetry when the page is hidden or discarded
function flushAnalytics() {
    const payload = JSON.stringify({ event: 'exit', duration: sessionMs });
    const blob = new Blob([payload], { type: 'application/json' });
    const queued = navigator.sendBeacon('/analytics', blob);
    if (!queued) {
        // Fallback: fetch with keepalive shares the same lifecycle guarantees
        fetch('/analytics', {
            method: 'POST',
            body: payload,
            headers: { 'Content-Type': 'application/json' },
            keepalive: true
        }).catch(() => {});
    }
}

document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') flushAnalytics();
});
window.addEventListener('pagehide', flushAnalytics);

Added 3 Aug 2026
Views 16
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 6 pings T 0 pings W 2 pings T 2 pings F
Applebot 2
Bing 2
Google 3 Bing 3 Applebot 2 ChatGPT 1 PetalBot 1
crawler 10
🧱 FUNDAMENTALS — new to this? Start with the ground floor.
JavaScript javascript The programming language of the browser — it reads and modifies the page (the DOM), reacts to user events, and fetches data without reloading.

JavaScript is the only language browsers execute, so every interactive behaviour on the web goes through it. Its two defining traits — single-threaded event loop and loose typing (== coercion) — explain the majority of both its bugs and its design patterns.

💡 Default to const, use === always, and reach for let only when a value genuinely reassigns.

Ask Codex about JavaScript →
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: Low
⚡ Quick Fix
Replace unload+fetch analytics with navigator.sendBeacon(url, blob) attached to visibilitychange (hidden) and pagehide events.
📦 Applies To
javascript ES2015 web browser
🔗 Prerequisites
🔍 Detection Hints
addEventListener\(\s*['"]unload['"][\s\S]{0,200}?(fetch\(|navigator\.sendBeacon)
Auto-detectable: ✓ Yes eslint
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: Medium ✗ Manual fix Fix: Low Context: Function Tests: Update


✓ schema.org compliant