Beaconing in Practice: An Update on Reliability and the Pending Beacon API
Table of Contents
- Introduction
- Pending Beacon API
- Why Pending Beacons?
- Pending Beacon Experiments
- Misc Findings
- Follow-Ups
- TL;DR
Introduction
A few years ago, I wrote an article titled Beaconing In Practice that covered all of the aspects of sending telemetry from your web app to a back-end server for analysis (aka "beaconing").
While the contents of that article are still relatively fresh and accurate, there are two new aspects of beaconing that I would like to cover in this post:
- The new Pending Beacon API
- The measured reliability of using
XMLHttpRequest
(XHR) vs.sendBeacon()
vs. Pending Beacon for sending data
Pending Beacon API
The Pending Beacon API is an exciting new proposal from Google Chrome engineers.
The goal is to provide an API for developers where they can "queue" data to be sent when a page is being unloaded (by the browser, automatically), rather than requiring developers to explicitly send beacons themselves in events like pagehide
or visibilitychange
(which don’t always fire reliably).
It is meant to be similar to the navigator.sendBeacon()
API, with a simple calling style.
Here’s an example of using the Pending Beacon API to send a beacon when the page is being hidden/unloaded (or a maximum of 60 seconds after "now"):
// queue a beacon for the unloading or +60s
var beacon = new window.PendingGetBeacon(
beaconUrl,
{
timeout: 60000,
backgroundTimeout: 0
});
(note the above API shape is outdated and the Pending Beacon API will utilize fetch()
in future versions)
The API is still being discussed, and is actively evolving based on community and browser vendor feedback.
If you want to experiment with the Pending Beacon in Chrome today, you can register for an Origin Trial for Chrome 107-115. Though, again, note that the current API shape (with the window.PendingGetBeacon
and window.PendingPostBeacon
interfaces) is evolving towards being an option of fetch()
instead.
Why Pending Beacons?
One of the challenges highlighted in the Beaconing In Practice article is how to reliably send data once it’s been gathered in a web app.
Developers frequently use events such as beforeunload
/unload
or pagehide
/visibilitychange
as a trigger for beaconing their data, but these events are not reliably fired on all platforms. If the events don’t fire, the beacons don’t get sent.
For example, if you want to gather all of your data and only send it once as the page is unloading, registering for all 4 of those events will only give you ~82.9% reliability in ensuring the data arrives at your server, even when using the sendBeacon()
API.
So, wouldn’t it be lovely if developers had a more reliable way of "queuing" data to be sent, and have the browser automagically send it once the page starts to unload? That’s where the Pending Beacon API comes in.
The Pending Beacon API gives developers a way to build a "pending" beacon. That pending beacon can then be mutated over time, or later discarded. The browser will then handle sending it (in its latest state) when the page is being hidden or unloading, so developers no longer need to listen to the beforeunload
/unload
/pagehide
/visibilitychange
events.
Ideally, Pending Beacon will be a mechanism that can replace usage of sendBeacon()
in browsers that support it, giving more reliable delivery of beacon data and better developer ergonomics (by not having to listen for, and send data during, unload-ish events).
Pending Beacon Experiments
Given those goals, I was curious to see how reliable Pending Beacon would be compared to existing APIs like XMLHttpRequest
(XHRs) or the sendBeacon()
API. I performed three experiments comparing how reliably data arrived after using one of those APIs in different scenarios.
Let’s explore three questions:
- Can we swap PendingBeacon in for usage of XHR and/or
sendBeacon()
in unload event handlers? - How reliable is asking PendingBeacon to send data "now" vs with a
backgroundTimeout
? - How reliable is queuing PendingBeacon data to be sent at page unload vs. listening to event handlers and using
sendBeacon()
in them?
Methodology
Over the course of a month, on a site that I control (with approx 2M page views), I ran an experiment gathering data from browsers using the following three APIs:
All of these APIs sent a small GET request back to the same domain / origin.
For all of the data below, I am only looking at Chrome and Chrome Mobile v107-115 (per the User-Agent
string) with support for window.PendingGetBeacon
, to ensure a level playing field. The data in Beaconing In Practice looks at reliability across all User-Agents, but the experiments below will focus solely on browsers supporting the Pending Beacon API.
Note that all of these tests were done with the PendingGetBeacon
interface, before the current proposal to have this be a fetch()
option. I’m unsure how the most recent proposal will affect these results, but I will re-do the test once that fetch()
update is available.
Reliability of XMLHttpRequest
vs. sendBeacon()
vs. Pending Beacon in Event Handlers
The first question I wanted to know was: Can Pending Beacon be easily swapped into existing analytics libraries (like boomerang.js) to replace sendBeacon()
and XMLHttpRequest
(XHR) usage, and retain the same (or better) reliability (beacon received rate)?
In boomerang for example, we listen to beforeunload
and pagehide
to send our final "unload" beacon. Can we just use Pending Beacon instead?
For this experiment, I segmented visitors into 3 equally-distributed A/B/C groups (given Pending Beacon API support):
- A: Force
PendingGetBeacon
(with{ timeout: 0, backgroundTimeout: -1 }
so it was sent immediately) - B: Force
navigator.sendBeacon()
- C: Force
XMLHttpRequest
Each group then attempted to send 6 beacons per page load:
- Immediately in the
<head>
of the HTML - In the page
onload
event - In the page
beforeunload
event - In the page
unload
event - In the page
pagehide
event - In the page
visibilitychange
event (forhidden
)
By seeing how often each of those beacons arrived, we can consider the reliability of each API, during different page lifecycle events. I’m only showing data for page loads where the first step (sending data immediately in the <head>
) occurred.
Let’s break the experimental data down by event first:
onload
The onload
event is probably the most common event for an analytics library to fire a beacon. Marketing and performance analytics tools will often send their main payload at that point in time.
Based on our experimentation, when firing a beacon just at the onload event, sendBeacon()
seems slightly more reliable than XHR, which is slightly more reliable than PendingGetBeacon
.
sendBeacon()
being more reliable than XHR is expected — the whole point of sendBeacon()
is to allow the browser to send data asynchronously of the page, in case it unloads after the beacon is queued up.
However, I’m surprised that PendingGetBeacon
appears to be the least reliable (by about 1% less than XHR), at least from my experiments.
Broken down by Desktop and Mobile:
Desktop is able to deliver beacons more reliably across all 3 APIs than mobile. On mobile, PendingGetBeacon
is about 2.8% less reliable than sendBeacon()
.
Note: that the above results are for only measuring a beacon sent immediately during the page’s onload
event, without accounting for any abandons that happen prior to onload
. That is why these numbers are so low — if a user abandoned the page prior to the onload
event, they would not be counted in the above chart. See the additional breakdowns below for how these numbers change if you use the suggested abandonment strategy of listening to onload
, pagehide
and visibilitychange
.
I was hoping the Pending Beacon API would be at-least-or-better reliable than sendBeacon()
, so I think there’s something to investigate here.
pagehide
or visibilitychange
If the intent is to measure events that occur in the page beyond the onload
event, i.e. additional performance or reliability metrics (such as Core Web Vitals or JavaScript errors), tools can send a beacon during one of the page’s unload events, such as beforeunload
, unload
, pagehide
or visibilitychange
.
Our recommended strategy is to listen to just pagehide
and visibilitychange
(for hidden), and not listen to the beforeunload
or unload
events (which are less reliable and can break BFCache navigations).
So let’s look at the result of sending a beacon immediately during a pagehide
or visibilitychange
event (if a beacon was received for either event):
This is showing that sendBeacon()
is still reigning supreme for reliability (95.8%), with PendingGetBeacon
slightly behind (89.1%) and XHR trailing that (84.9%).
However, when we break it down by Desktop:
PendingGetBeacon
is nearly as reliable as sendBeacon()
, with XHR
trailing behind, while on Mobile:
There appears to be a huge drop-off in reliability for PendingGetBeacon
on Mobile vs. Desktop.
Possibly a bug with Pending Beacon in Chrome’s initial implementation here? This data would give me pause in swapping to Pending Beacon right now.
onload
or pagehide
or visibilitychange
Finally, let’s combine the above three events per the suggested abandonment strategy, and see how reliable each API is if we’re listening for all 3 events (and sending data once in any of them).
Of course, this increases the reliability of receiving beacons to the maximum possible, with sendBeacon()
able to get a beacon to the server 98% of the time:
Broken down by Desktop vs. Mobile, we see that Desktop is has an extremely high rate of receiving beacons:
While Mobile continues to show a possible issue with PendingGetBeacon
vs. sendBeacon()
(a 7.7% drop-off)!
Conclusion
From this experiment at least, it appears sendBeacon()
continues to be the most reliable way of sending beacon data.
If sending data during onload
, sendBeacon()
is slightly more reliable than PendingGetBeacon
.
However, there appears to be a bug with PendingGetBeacon
during a page-unloading scenario like pagehide
or visibilitychange
, particularly on Mobile. If the Chrome engineers can figure out a way to increase the reliability there, I would expect the Pending Beacon API to be equivalent to using sendBeacon()
(which is our preferred mechanism today).
NOTE: I measured the reliability of sending beacons during beforeunload
and unload
as well, but since those events are deprecated / not-recommended / unreliable / break BFCache events, I’ll skip those results in this post.
Reliability of Pending Beacon "now" vs "backgroundTimeout"
The next experiment I ran was to determine if the backgroundTimeout
functionality of the Pending Beacon API was reliable to use.
Here’s the description of the parameter (which has changed slightly with the fetch()
-based proposal, but I would guess would operate similarly):
backgroundTimeout
: A mutable Number property specifying a timeout in milliseconds whether the timer starts after the page enters the next hidden visibility state. If setting the value >= 0, after the timeout expires, the beacon will be queued for sending by the browser, regardless of whether or not the page has been discarded yet. If the value < 0, it is equivalent to no timeout and the beacon will only be sent by the browser on page discarded or on page evicted from BFCache. The timeout will be reset if the page enters visible state again before the timeout expires. Note that the beacon is not guaranteed to be sent at exactly this many milliseconds after hidden, because the browser has freedom to bundle/batch multiple beacons, and the browser might send out earlier than specified value (see Privacy Considerations). Defaults to -1.
In other words, ask the browser to send a beacon after backgroundTimeout
milliseconds of being hidden.
This can be very useful as an alternative to listening to the pagehide
/ visibilitychange
events for beaconing your "last" bits of data. If you regularly update your Pending Beacon, you may not need to listen to those event at all.
But can we trust the browser to still send our Pending Beacon, after we’ve queued it up?
For this experiment, I segmented visitors into 2 equally-distributed A/B groups (given Pending Beacon API support):
- A: Force
PendingGetBeacon
to send a beacon now (with{ timeout: 0, backgroundTimeout: -1 }
- B: Force
PendingGetBeacon
to send a beacon after 60s or when the page is hidden (with{ timeout: 60000, backgroundTimeout: 0 }
We are considering doing something similar to group B for boomerang.js, i.e. send all beacons within 60 seconds of the page load (so the data is still "real-time fresh" in dashboards), and asking the browser to send the data immediately if the user navigates away or closes the browser before then.
Let’s look at the results of using PendingGetBeacon
to send a beacon "now" vs. "when the page is hidden/unloads":
Given a baseline of 100% meaning we received a "now" PendingGetBeacon
, we’re seeing the 60s timeout + @hidden beacon about 98.5% of the time across Desktop and Mobile.
Desktop is slightly more reliable (99.7%) vs. Mobile (96.4%).
I think this is a great result, confirming the value-add of PendingGetBeacon
. Instead of having to add event listeners for pagehide
visibilitychange
and a 60s setTimeout()
, the browser delivered the beacon very reliably on its own!
Remember, listening to all 4 unload-ish events (beforeunload
, unload
, pagehide
, visibilitychange
) and sending a beacon in those events only resulted in ~82.9% reliability!
And in the meantime, the pending beacon could be manipulated to add/remove additional data up until the page unloads.
Reliability of Pending Beacon "backgroundTimeout" once vs. sendBeacon()
in Event Handlers
Given that the last experiment showed that Pending Beacon with backgroundTimeout
was very reliable in sending beacons at page unload, what is the difference between using PendingGetBeacon
with backgroundTimeout: 0
vs. listening for pagehide
and visibilitychange
and sending a beacon with sendBeacon()
?
Great news! Not only is the PendingGetBeacon
more ergonomic (not having to listen for pagehide
and visibilitychange
events), it’s more reliably sending data when the page is unloading.
One interesting result I see here, is that the PendingGetBeacon
reliability with backgroundTimeout: 0
was more reliable than listening to pagehide
and visibilitychange
and using PendingGetBeacon
(now) in those events directly. This is likely due to the fact that pagehide
and visibilitychange
aren’t 100% reliable in the first place, but I would hope for it to be as-close-to sendBeacon()
reliable as possible.
Misc Findings
- There’s currently no way to debug Pending Beacon in Chrome Developer Tools — outgoing beacons are not visible in the Network tab. This makes it very hard to debug or verify that the feature is working. When debugging issues with boomerang.js I am constantly reviewing the outgoing beacon, so not having visibility into Dev Tools would be a huge hinderance. There’s an Github issue tracking this.
- While reviewing the data, I found some Samsung Internet browser data in the data-set, indicating that it supported
window.PendingGetBeacon
.- However, I only received data for
XMLHttpRequest
andsendBeacon()
beacons. - Does this mean Samsung Internet browser (which is Chromium-based) is registering
window.PendingGetBeacon
but not fully implementing the beacon sending? I will need to investigate more.
- However, I only received data for
Follow-Ups
First, I want to say that my experiments and conclusions probably have some flaws. I’ve reviewed and re-reviewed my methodology and queries several times, but I am a human (I think!) and make mistakes. I’m hoping others can review this data.
Given that, some follow-ups I plan on doing based on the above findings:
- Re-do this analysis once the interface is changed to be
Fetch
-based - Review the reliability data with the Chrome engineers to see if we should file bugs for any of the drops in reliability vs.
sendBeacon()
, in particular:- During
onload
Pending Beacon (withtimeout:0
) is about 1.2% less reliable thansendBeacon()
- During
pagehide
andvisibilitychange
Pending Beacon (withtimeout:0
), on Mobile, is about 17.9% less reliable thansendBeacon()
- During
- Review why the Samsung Internet browser is registering the
window.PendingGetBeacon
interface but not sending any beacons (was there an error I wasn’t catching?)
TL;DR
- I’m really excited for the Pending Beacon API. I think it’s going to developers better ergonomics for sending data, and a more reliable way to send beacons at the end of the page lifetime
- The new Pending Beacon API is in active development and going through a feedback and Origin Trial cycle
- There may be some small reliability issues vs.
sendBeacon()
that should be investigated before widespread adoption - The
navigator.sendBeacon
API still seems to be the most reliable mechanism for sending beacons, if you’re queuing up data to be sent inpagehide
orvisibilitychange
events