Home | Send Feedback | Share on Bluesky |

A closer look at the Beacon API

Published: 5. September 2018  •  Updated: 20. October 2025  •  javascript

The Beacon API is a browser API that maybe few people have heard of or use in their daily work, unless they do a lot of analytics reporting in the browser.

The Beacon API is very well-supported in modern browsers, including Chrome, Edge, Firefox, Safari, and many mobile browsers. For detailed browser support information, visit the caniuse.com site: https://caniuse.com/#feat=beacon

The API is straightforward and consists of just one method in the navigator object.

  navigator.sendBeacon(url [, data]);

The Beacon API sends a POST request to the URL you provide as the first parameter, with optional data that you pass as the second parameter. The Beacon API supports ArrayBufferView, Blob, String, and FormData as the second parameter. The API does not expect a response; if your back end sends one, the browser ignores it.

The browser tries to send the request immediately after internally queueing it. The Beacon API only tries to send the request once, and it does not care if the URL is wrong, the server is not responding, sends an error back, or if the browser is offline. The Chrome browser prints an error message into the console and then deletes the request from the queue. There is no retry mechanism built-in, nor does the browser queue these requests while offline and then send them the next time the device is online.

The purpose of the Beacon API is to send small amounts of data to a server in the background, without blocking the loading of a page or other critical operations. The primary use case is to send analytics data when a user navigates away from a page. In this situation, traditional AJAX calls may be canceled by the browser because the page is unloading, and the data may never reach the server. The Beacon API solves this problem by allowing developers to send data asynchronously, even when the page is unloading.

Usage

An application can call the sendBeacon method without any data.

    navigator.sendBeacon('heartbeat');

usage.html

The first parameter represents a standard URL. You can send data with query parameters.

    navigator.sendBeacon('heartbeat?id=123');

usage.html

The second parameter of the sendBeacon method data is string.

    // String
    const data = JSON.stringify({
      location: window.location,
      time: Date()
    });
    navigator.sendBeacon('usageString', data);

usage.html

or a FormData object.

    // FormData
    const formData = new FormData();
    formData.append("session", "12345");
    formData.append("id", 11);
    navigator.sendBeacon('usageFormData', formData);

usage.html

or a Blob.

    // Blob
    const ua = JSON.stringify({ ua: navigator.userAgent, now: performance.now() });
    const headers = { type: 'application/json' };
    const blob = new Blob([ua], headers);
    navigator.sendBeacon('usageBlob', blob);

usage.html

or an ArrayBufferView. In this example, a Uint8Array.

    // ArrayBufferView
    const string = '=======This is a text, sent compressed to the server=======';
    const enc = new TextEncoder();
    const encoded = enc.encode(string);
    const compressed = pako.deflate(encoded);

    navigator.sendBeacon('usageArrayBufferView', compressed);

usage.html

sendBeacon() sends a POST request with the data as the body to the server. There is no way to change the HTTP method; the Beacon API only supports POST requests, and it does not expect a response.

I wrote a Spring Boot backend for testing these examples. You can find the source code that handles these requests here: https://github.com/ralscha/blog/blob/master/beacon/src/main/java/ch/rasc/beacon/UsageController.java

Examples

Next, we will look at a few examples that use the Beacon API to send data from a browser to a server. For all these examples, I use a Spring Boot backend. You can find the source code here: https://github.com/ralscha/blog/blob/master/beacon/src/main/java/ch/rasc/beacon/ExamplesController.java


Page Lifecycle

The first example uses the Page Visibility API to detect when a user navigates away from a page.

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Lifecycle: Page 1</title>
</head>

<body>
  <h1>Lifecycle: This is Page 1</h1>

  <div>
    <a href="page2.html">Go to Page 2</a>
  </div>
  <script src="main.js"></script>
</body>

</html>

index.html

The example listens for the visibilitychange event and checks if the document's visibility state is hidden. If so, it sends some analytics data to the server with the Beacon API.

const analytics = { start: performance.now() };

document.addEventListener('visibilitychange', event => {
  analytics.visibility.push({ state: document.visibilityState, ts: event.timeStamp });
  
  if (document.visibilityState === 'hidden') {
    analytics.stop = performance.now();
    analytics.hiddenAt = event.timeStamp;
    navigator.sendBeacon('../lifecycle', JSON.stringify(analytics));
  }
});


main.js


Performance

The second examples uses the Performance API to collect performance metrics of a web page. The examples uses a PerformanceObserver to listen for performance entries like navigation, paint, and largest-contentful-paint. Whenever a new performance entry is available, the observer callback function is called.

const performanceData = {
  navigationStart: null,
  domContentLoadedEnd: null,
  loadComplete: null,
  fcp: null, // First Contentful Paint
  lcp: null, // Largest Contentful Paint
  fid: null, // First Input Delay
  timestamp: null
};

const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const navigationTiming = performance.getEntriesByType('navigation')[0];  
  
  if (navigationTiming) {
    performanceData.navigationStart = navigationTiming.fetchStart;
    performanceData.domContentLoadedEnd = navigationTiming.domContentLoadedEventEnd;
    performanceData.loadComplete = navigationTiming.loadEventEnd;
    performanceData.timestamp = Date.now();
  }
  
  entries.forEach(entry => {
    switch (entry.entryType) {
      case 'paint':
        if (entry.name === 'first-contentful-paint') {
          performanceData.fcp = entry.startTime;
        }
        break;
      case 'largest-contentful-paint':
        performanceData.lcp = entry.startTime;
        break;
      case 'first-input':
        performanceData.fid = entry.processingStart - entry.startTime;
        break;
    }
  });
  
  performanceData.timestamp = Date.now();
  navigator.sendBeacon('../performance', JSON.stringify(performanceData));
});

observer.observe({ entryTypes: ['navigation', 'paint', 'largest-contentful-paint', 'first-input'] });

main.js


Web Vitals

In the previous example, we used the Performance API to collect performance metrics. Another option is to use the Web Vitals library from Google. The library provides an easy way to collect essential performance metrics like Largest Contentful Paint (LCP), First Input Delay (FID), and Cumulative Layout Shift (CLS). The library works in all modern browsers and provides a simple API to get the metrics.

    import {onCLS, onINP, onLCP} from 'https://unpkg.com/web-vitals@5?module';

    function sendToAnalytics(metric) {
    const body = JSON.stringify({
        name: metric.name,
        value: metric.value,
        rating: metric.rating,
        delta: metric.delta,
        id: metric.id,
        navigationType: metric.navigationType,
        attribution: metric.attribution ? JSON.stringify(metric.attribution) : null 
    });

    navigator.sendBeacon('../webVitals', body);
    }

    onCLS(sendToAnalytics);
    onINP(sendToAnalytics);
    onLCP(sendToAnalytics);

index.html

For more information about the Web Vitals library, visit the GitHub repository.


Global Error Handling

When a browser encounters an error, it prints a message into the browser console. For the developers, this is not very useful because they usually cannot look at the user's computer. To solve that, we can send the messages that the browser emits to the server. On the server, we can store them in a database, send emails to the developers, or automatically open a ticket in an issue management system.

To install a global error handler, we only have to listen for the error event on the window object.

    window.onerror = function (msg, url, line, col, error) {
      const formData = new FormData();
      formData.append('url', url);
      formData.append('line', line);
      formData.append('col', col);
      formData.append('error', error);
      navigator.sendBeacon('../clientError', formData);
    };

index.html

The error handler receives these parameters that describe the error:

Alternatively, we can install a global error handler with addEventListener. In this case, the handler method has a different signature and only receives an event object.

   window.addEventListener('error', event => { ... })

The event object contains these read-only properties:
event.message, event.filename, event.lineno, event.colno, and event.error. They correspond to the parameters of the onerror handler.


Position

With the Geolocation API, a web application can access the position of a user. For privacy reasons, the browser must ask the user if they want to permit the reporting of their location data.

If the user gives permission, we can listen for location changes with the navigator.geolocation.watchPosition method. The example then sends the latitude, longitude, and timestamp with the Beacon API to the server.

      navigator.geolocation.watchPosition(pos => {

        document.getElementById('latitude').innerHTML = pos.coords.latitude;
        document.getElementById('longitude').innerHTML = pos.coords.longitude;

        const position = {
          latitude: pos.coords.latitude,
          longitude: pos.coords.longitude,
          ts: pos.timestamp
        };
        navigator.sendBeacon('../position', JSON.stringify(position));
      });

index.html

Visit MDN for more information about the GeoLocation API.


Reporting Observer

If you are surfing the web with Chrome and you look into the browser console, you may have already noticed some [Intervention] and [Deprecation] warnings.

Like error messages, these warnings could be useful for a developer to improve their application. Unfortunately, we cannot see them on the users' browsers, and users do not notice them because they usually don't open the browser console.

Unfortunately, the error event handler does not capture these messages, because these warnings are generated directly by the browser. The error handler is only called for runtime errors caused by our code.

The ReportingObserver API offers a way to programmatically access browser warnings and interventions. For further details, refer to this article.

    const observer = new ReportingObserver((reports, observer) => {
      for (const report of reports) {
        navigator.sendBeacon('../reportObserver', JSON.stringify(report.body, ['id', 'columnNumber', 'lineNumber', 'message', 'sourceFile']));
      }
    }, { types: ['intervention', 'deprecation'], buffered: true });

    observer.observe();

index.html

Each time the browser issues a warning, the callback function passed to the ReportingObserver constructor is called.

Visit this page to learn more about the ReportingObserver API.


Generic Sensor API

The Generic Sensor API provides access to device sensors like accelerometers. For current browser support and more information, check out this page.

In this example, we access the accelerometer, listen for the reading event, and send the sensor data to the server.

    const sensor = new Accelerometer();
    sensor.start();

    sensor.onreading = () => {
      const formData = new FormData();
      formData.append('x', sensor.x);
      formData.append('y', sensor.y);
      formData.append('z', sensor.y);
      navigator.sendBeacon('../sensor', formData);

      document.getElementById('x').innerText = sensor.x;
      document.getElementById('y').innerText = sensor.y;
      document.getElementById('z').innerText = sensor.z;
    };

index.html

Conclusion

This concludes our exploration of the Beacon API and its use cases. The Beacon API is useful for sending data in the background from the browser to a server.

Browser Compatibility: The Beacon API has excellent support in modern browsers. However, always test your implementation thoroughly across different browsers and versions to ensure consistent functionality.

Data Limits: The Beacon API is designed for sending small amounts of data. There are limits to the size of data that can be sent in a single beacon request. While the exact limits vary by browser, they are typically around 64KB.

Security: If you are sending sensitive data using the Beacon API, ensure appropriate security measures are in place on your server.

Alternative: In many cases, instead of the Beacon API, you can send data with the Fetch API. Consider this if you need more control over the request or require a response from the server.