7.1 Service workers and beacons: detection and safe forwarding
The keepalive-POST-empty-destination detection triple with its honest false positives, and the buffer-then-re-issue patterns that never lose data.
Advanced
Beacons are ordinary fetches from the network's point of view, and that means a service worker's
fetch event sees them — a page's telemetry passes through the same interceptor as its API calls. Interception is powerful (offline buffering, rewriting, deduplication, absorption) and dangerous in exact proportion, because the beacon contract gives the page no feedback loop: a service worker that mishandles a beacon loses data invisibly, with
true already returned upstream.
What the worker sees
Inside the handler,
event.request is a
Request object shaped by the fixed recipe you met in part one:
method POST,
keepalive true,
mode
no-cors (for safelisted payloads),
credentials include — and
destination the empty string, because beacons load nothing. What the worker
cannot see is the initiator: the Fetch standard tracks an initiator type per request — and
"beacon" is literally one of its values — but that field is internal bookkeeping for Resource Timing, CSP reports, and DevTools; the
Request class exposes no
initiator property, and Resource Timing's
initiatorType is not readable from inside the worker's fetch event either.
The detection triple and its honest false positives
The best JS-visible signature is the triple:
keepalive === true && method === 'POST' && destination === ''. The runnable example builds
real
Request objects — including an actual beacon-shaped one, constructible because
keepalive is a settable
RequestInit member — and walks the predicate across every interesting shape, false positives included:
Output
Ready to run.
The one ambiguous row is the honest cost of the design: a hand-rolled
fetch(url, {method:'POST', keepalive:true}) — the worker-side substitute from earlier in this series — is indistinguishable from a beacon, because a beacon
is that request. When the predicate gates destructive behavior (absorbing or rewriting), scope it by URL pathname as the example's second function does; when it merely logs, the triple alone is fine. The false-positive surface is bounded, known, and permanent — the platform chose not to expose initiator, and code that plans around that fact stays correct.
Forwarding: the patterns that do not lose data
The dangerous moment is forwarding, and one spec fact writes all the rules:
event.request.body is a
ReadableStream, and streams throw
TypeError under keepalive. So the intuitive forwarding —
fetch(url, {method: 'POST', body: event.request.body, keepalive: true}) — is broken by construction. The second trap is timing:
respondWith must be called synchronously in the handler;
awaiting before it can end the event before your response exists. The safe core is buffer-then-re-issue, and it runs below against real
Request objects exactly as the worker would handle them:
Output
Ready to run.
Three shipping patterns grow from that core.
Pass-through:
event.respondWith(fetch(event.request)) forwards the original untouched — no body read, no stream problem, keepalive semantics preserved by the platform.
Buffered re-issue:
reissueKeepalive(event.request, overrides) clones, reads the body to a string, and constructs a fresh keepalive request — the modify/redirect path, at the cost of materializing the body (bounded by the 64 KiB quota anyway).
Short-circuit:
event.respondWith(new Response(null, {status: 204})) absorbs the beacon and answers it locally — the spec's own encouraged response shape, from inside the worker. Each pattern is one line of handler code around the tested core; the example's closing comment shows the wiring.
Quick check
Which forwarding call is spec-safe inside a fetch handler?
self.addEventListener('fetch', (event) => {
event.respondWith();
});
Key takeaways
-
Beacons hit the
fetchevent like any request: POST, keepalive, no-cors, empty destination — but no JS-visible initiator. -
Detect with
keepalive && POST && destination === ''; the hand-rolled keepalive fetch is a permanent, known false positive — scope by URL when it matters. -
Never forward
event.request.bodywith keepalive: streams throw TypeError; buffer viaclone().text()and re-issue. -
Call
respondWithsynchronously; async work happens inside the promise you hand it. -
Short-circuit with
204— the spec's own encouraged response — and treat intercepted beacons as data you now own.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.