Sending Background Data with the Beacon API

Page navigation shouldn’t be held hostage by analytics requests. When users click a link or close a tab, traditional HTTP requests can block or fail entirely, leaving you with incomplete data and frustrated users. The Beacon API solution changes this by queuing requests in the background, ensuring your data reaches the server without impacting performance.
This article demonstrates how to implement navigator.sendBeacon()
for reliable background data transmission. You’ll learn practical applications like analytics tracking and error reporting, understand why it outperforms fetch()
during page transitions, and see a complete implementation using modern JavaScript.
Key Takeaways
- The Beacon API queues requests in the background without blocking page navigation
- Use
navigator.sendBeacon()
for analytics, error reporting, and user action tracking - Beacons are more reliable than
fetch()
during page unload events - Keep payloads small and batch multiple events for optimal performance
- Implement fallbacks for older browsers that don’t support the Beacon API
- The API returns a boolean indicating successful queuing, not delivery confirmation
What Makes the Beacon API Different
The Beacon API approach solves a fundamental problem: traditional HTTP requests block page navigation. When you use fetch()
or XMLHttpRequest
during unload events, the browser must wait for the request to complete before allowing navigation to proceed. This creates a poor user experience and unreliable data transmission.
The Beacon API operates differently. It queues requests asynchronously and handles transmission in the background, even after the page has closed. The browser manages the entire process without blocking navigation or requiring your JavaScript to remain active.
Why Traditional Methods Fail During Page Transitions
Consider this common scenario using fetch()
:
window.addEventListener('beforeunload', () => {
// This may fail or block navigation
fetch('/analytics', {
method: 'POST',
body: JSON.stringify({ event: 'page_exit' })
})
})
This approach has several problems:
- The request may be cancelled when the page unloads
- Navigation is blocked until the request completes
- No guarantee the data will reach the server
- Poor user experience with delayed page transitions
How navigator.sendBeacon() Works
The navigator.sendBeacon()
method accepts two parameters: a URL endpoint and optional data. It returns a boolean indicating whether the request was successfully queued (not whether it reached the server).
const success = navigator.sendBeacon(url, data)
The browser handles the actual transmission, optimizing for network conditions and system resources. This fire-and-forget approach is perfect for analytics, logging, and diagnostic data where you don’t need a response.
Practical Implementation Examples
Basic Analytics Tracking
Here’s a minimal implementation for tracking user sessions:
// Track session data
const sessionData = {
userId: 'user123',
sessionDuration: Date.now() - sessionStart,
pageViews: pageViewCount,
timestamp: new Date().toISOString()
}
// Send data when page becomes hidden or user navigates away
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
const payload = JSON.stringify(sessionData)
navigator.sendBeacon('/log', payload)
}
})
window.addEventListener('beforeunload', () => {
const payload = JSON.stringify(sessionData)
navigator.sendBeacon('/log', payload)
})
Error Reporting System
The Beacon API excels at capturing and reporting JavaScript errors:
window.addEventListener('error', (event) => {
const errorData = {
message: event.message,
filename: event.filename,
line: event.lineno,
column: event.colno,
stack: event.error?.stack,
userAgent: navigator.userAgent,
timestamp: Date.now()
}
if (navigator.sendBeacon) {
navigator.sendBeacon('/log', JSON.stringify(errorData))
}
})
User Action Tracking
Track specific user interactions without blocking the interface:
function trackUserAction(action, details) {
const actionData = {
action,
details,
timestamp: Date.now(),
url: window.location.href
}
if (navigator.sendBeacon) {
navigator.sendBeacon('/log', JSON.stringify(actionData))
}
}
// Usage examples
document.getElementById('cta-button').addEventListener('click', () => {
trackUserAction('cta_click', { buttonId: 'cta-button' })
})
document.addEventListener('scroll', throttle(() => {
const scrollPercent = Math.round(
(window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
)
trackUserAction('scroll', { percentage: scrollPercent })
}, 1000))
Complete Analytics Implementation
Here’s a comprehensive example combining multiple tracking scenarios:
class AnalyticsTracker {
constructor(endpoint = '/log') {
this.endpoint = endpoint
this.sessionStart = Date.now()
this.events = []
this.setupEventListeners()
}
setupEventListeners() {
// Track page visibility changes
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.sendBatch()
}
})
// Track page unload
window.addEventListener('beforeunload', () => {
this.sendBatch()
})
// Track errors
window.addEventListener('error', (event) => {
this.trackEvent('error', {
message: event.message,
filename: event.filename,
line: event.lineno
})
})
}
trackEvent(type, data) {
this.events.push({
type,
data,
timestamp: Date.now()
})
// Send batch if we have too many events
if (this.events.length >= 10) {
this.sendBatch()
}
}
sendBatch() {
if (this.events.length === 0) return
const payload = {
sessionId: this.generateSessionId(),
sessionDuration: Date.now() - this.sessionStart,
events: this.events,
url: window.location.href,
userAgent: navigator.userAgent
}
if (navigator.sendBeacon) {
const success = navigator.sendBeacon(
this.endpoint,
JSON.stringify(payload)
)
if (success) {
this.events = [] // Clear sent events
}
}
}
generateSessionId() {
return Math.random().toString(36).substring(2, 15)
}
}
// Initialize tracker
const tracker = new AnalyticsTracker('/log')
// Track custom events
tracker.trackEvent('page_view', { page: window.location.pathname })
Browser Support and Fallbacks
The Beacon API has excellent browser support across modern browsers. For older browsers, implement a graceful fallback:
function sendData(url, data) {
if (navigator.sendBeacon) {
return navigator.sendBeacon(url, data)
}
// Fallback for older browsers
try {
const xhr = new XMLHttpRequest()
xhr.open('POST', url, false) // Synchronous for unload events
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(data)
return true
} catch (error) {
console.warn('Failed to send data:', error)
return false
}
}
Best Practices for Beacon Implementation
Keep Payloads Small
The Beacon API is designed for small data packets. Limit payloads to essential information:
// Good: Focused, essential data
const essentialData = {
event: 'conversion',
value: 29.99,
timestamp: Date.now()
}
// Avoid: Large, unnecessary data
const bloatedData = {
event: 'conversion',
value: 29.99,
timestamp: Date.now(),
entireDOMState: document.documentElement.outerHTML,
allCookies: document.cookie,
completeUserHistory: getUserHistory()
}
Batch Multiple Events
Instead of sending individual beacons for each event, batch them together:
const eventBatch = []
function addEvent(eventData) {
eventBatch.push(eventData)
// Send batch when it reaches a certain size
if (eventBatch.length >= 5) {
sendBatch()
}
}
function sendBatch() {
if (eventBatch.length > 0) {
navigator.sendBeacon('/log', JSON.stringify(eventBatch))
eventBatch.length = 0 // Clear the batch
}
}
Handle Network Failures Gracefully
Since beacons don’t provide response feedback, implement client-side validation:
function validateAndSend(data) {
// Validate data structure
if (!data || typeof data !== 'object') {
console.warn('Invalid data for beacon')
return false
}
// Check payload size (browsers typically limit to 64KB)
const payload = JSON.stringify(data)
if (payload.length > 65536) {
console.warn('Payload too large for beacon')
return false
}
return navigator.sendBeacon('/log', payload)
}
Beacon API vs Fetch During Page Transitions
The key difference becomes apparent during page unload events:
// Beacon API: Non-blocking, reliable
window.addEventListener('beforeunload', () => {
navigator.sendBeacon('/log', JSON.stringify(data)) // ✅ Reliable
})
// Fetch API: May block or fail
window.addEventListener('beforeunload', () => {
fetch('/log', {
method: 'POST',
body: JSON.stringify(data),
keepalive: true // Helps but doesn't guarantee success
}) // ❌ May fail during navigation
})
The keepalive
flag in fetch requests provides similar functionality but with less reliability than the Beacon API, which was specifically designed for this use case.
Conclusion
The Beacon API provides a robust solution for background data transmission without blocking page navigation. By queuing requests asynchronously, it ensures reliable data delivery while maintaining optimal user experience. Whether you’re implementing analytics tracking, error reporting, or user action logging, navigator.sendBeacon()
offers the reliability and performance that traditional HTTP methods cannot match during page transitions.
The fire-and-forget nature of beacons makes them ideal for scenarios where you need to send data but don’t require a response. Combined with proper batching strategies and payload optimization, the Beacon API becomes an essential tool for modern web applications that prioritize both data collection and user experience.
FAQs
The Beacon API is a JavaScript web standard that sends small amounts of data to a server without waiting for a response. Unlike fetch or XMLHttpRequest, beacon requests are queued by the browser and sent asynchronously, making them perfect for analytics and logging during page navigation events where traditional requests might fail or block user experience.
No, the Beacon API is designed for small data packets, typically limited to 64KB by most browsers. If you need to send larger amounts of data, consider batching smaller requests or using alternative methods like the Fetch API with the keepalive flag for non-critical timing scenarios.
The Beacon API has excellent support in all modern browsers including Chrome, Firefox, Safari, and Edge. It does not work in Internet Explorer. For older browser support, implement a fallback using XMLHttpRequest or fetch, though these alternatives may not provide the same reliability during page unload events.
The navigator.sendBeacon method returns a boolean indicating whether the request was successfully queued by the browser, not whether it reached the server. Since beacons are fire-and-forget, you cannot access the server response or know definitively if the data was received. This is by design for optimal performance.
Use the Beacon API when you need to send data during page transitions, unload events, or when the page becomes hidden, and you do not need a response. Use fetch for regular API calls where you need to handle responses, errors, or when the timing is not critical to page navigation.