A monitoring SDK boldogan kiírja, hogy navigator.sendBeacon() visszatért true-val. A dashboardon mégis lyukas a session vége. A hálózat lassú volt, a felhasználó bezárta a laptopot, a mobil OS pedig kilőtte a böngészőt. A boolean közben végig igazat mondott – csak mi kérdeztünk tőle olyat, amire nem válaszol.
A sendBeacon() nem kézbesítési bizonylat. Arra jó, hogy egy kis POST törzset a böngésző gondjaira bízzunk anélkül, hogy feltartanánk a navigációt. Telemetriához remek. Fizetés véglegesítéséhez nagyjából olyan megnyugtató, mint a „feladtam postán” mondat nyomkövetési szám nélkül.

A true csak annyi: befért a sorba
A Beacon specifikáció meglepően egyértelmű: a visszatérési érték azt jelzi, hogy a user agent sorba tudta-e állítani az adatot. A tényleges hálózati átvitel aszinkron, eredményéről az API semmit nem mond. Nincs response, nincs callback, nincs retry-ígéret. Offline állapothoz sem ad külön tárolást; attól, hogy beaconnek hívják, még nem növeszt saját postagalambot.
Ha false-ot kapsz, legalább biztosan tudod, hogy az adat nem került a sorba. A tipikus ok a méretkorlát. A true viszont csak egy köztes állapot: a böngésző vállalta a próbálkozást, de hálózati hiba, processzkilövés vagy szerverhiba után nem fogja neked utólag bevallani, hogy nem sikerült.
Nem 64 KiB kérésenként, hanem közösen
Itt jön a „na, ezt nem tudtam” rész. A sendBeacon() a háttérben keepalive jelzővel indít fetch-et. A Fetch algoritmusa összeadja az adott fetch grouphoz tartozó, még be nem fejezett keepalive kérések törzsméretét. Ha az új törzzsel együtt az összeg nagyobb 64 KiB-nál, hálózati hibát ad. Vagyis ez nem kényelmes 64 KiB minden hívásra, hanem közös, gördülő keret.
Hat darab, egyenként 10 000 bájtos beacon még elfoglalhat 60 000 bájtot. A hetedik ugyanekkora már nem fér be, amíg az előzőek közül nem fejeződik be valamelyik. Ráadásul a kereten a sendBeacon() és a fetch(..., { keepalive: true }) is osztozik. A szabványos webplatformteszt ezt szó szerint próbálja: 65 536 bájt után még egy üres törzs befér, egyetlen további bájtnak viszont már false jár.
És bájtról beszélünk, nem JavaScript-karakterről. Az ASCII többnyire egy bájt, egy magyar ékezet vagy emoji UTF-8-ban több. A payload.length ezért rossz mérce; a Blob.size vagy a TextEncoder eredményének byteLength értéke kell.
const MAX_BATCH_BYTES = 48 * 1024; // saját, óvatos keret
let queue = [];
function makePayload(events) {
return new Blob([JSON.stringify(events)], {
type: "text/plain;charset=UTF-8",
});
}
function track(event) {
queue.push(event);
}
function flushTelemetry() {
if (queue.length === 0) return;
const body = makePayload(queue);
if (body.size > MAX_BATCH_BYTES) return false;
const queued = navigator.sendBeacon("/telemetry", body);
if (queued) queue = [];
// false esetén az adat maradjon meg a következő próbához
return queued;
}
A 48 KiB nem webszabvány, hanem alkalmazásszintű biztonsági tartalék: ugyanabban a dokumentumban más mérőkód is fogyaszthatja a közös keretet. A fontos rész az, hogy a batch méretét bájtban korlátozzuk, és csak sikeres sorba állítás után ürítsük a memóriabeli sort.
Mikor küldjük el?
Az unload eseményre épített „utolsó pillanat” régóta rossz fogadás: mobilon simán elmaradhat, és az unload handler a bfcache-t is ronthatja. A Page Visibility ajánlása szerint a hidden állapot az utolsó megbízhatóan megfigyelhető pont. Ekkor érdemes flusholni – de ne kizárólag ekkor. Egy hosszú életű SPA közben is küldj kisebb batch-eket, például idő- vagy méretküszöbnél.
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") flushTelemetry();
});
Ez sem varázslat. Ha az OS úgy lövi ki a háttérben lévő processzt, hogy JavaScript már nem futhat, nincs esemény, amit elkaphatnánk. Ami nem veszhet el, azt már használat közben tedd tartós kliensoldali outboxba, a szerveren pedig kezelj egyedi eseményazonosítót és deduplikációt. A „küldd el még egyszer” retry nélkül adatvesztés, idempotencia nélkül duplikáció. A hálózat ilyen udvarias.
És a fetch keepalive?
Ha más HTTP metódus, egyedi header vagy response kell, a fetch() keepalive opciója rugalmasabb. A Promise-ból még választ is kaphatsz, amíg az oldal él. Csakhogy ugyanaz a 64 KiB-os keret vonatkozik rá, és a bezárt dokumentum már nem fogja feldolgozni a Promise eredményét. A keepalive tehát nem tartós háttérmunka, csak engedély arra, hogy a kérés túlélhesse az oldalt.
A sendBeacon() ráadásul mindig POST, saját headert nem állíthatsz. Cross-origin célpontnál egy nem CORS-safelisted Content-Type – például application/json-os Blob – preflightot válthat ki. Telemetriánál ezért egyszerűbb a saját origin egyik végpontja és a text/plain törzs; a szerver attól még nyugodtan parse-olhatja JSON-ként. Nem elegáns? Talán. Legalább működés közben nem kér még egy engedélykérő kérést a kérés elé.
Production-ítélet
Használnám productionben analitikára, teljesítménymérésre és diagnosztikára, ahol néhány elveszett esemény elfogadható. Kis batch-eket küldenék menet közben, visibilitychange-nél még egyszer flusholnék, figyelném a false arányát, és a backend minden eseményt azonosító alapján deduplikálna.
Nem használnám rendelés, fizetés, jogosultságmódosítás vagy auditnapló egyetlen kézbesítési csatornájaként. Ott szerveres visszaigazolás, tartós outbox és retry kell. A beacon jó futár az apró, részben eldobható csomagokra. Csak tértivevényt ne képzeljünk a kezébe.