TL;DR, Resposta rápida
8 min de leituraA distância entre sendBeacon vs fetch keepalive está no controle, não na entrega. navigator.sendBeacon() enfileira um POST que sobrevive à página e devolve apenas true ou false, com um Content-Type que o navegador deduz do tipo de corpo que você passa. fetch() com keepalive em true acrescenta cabeçalhos próprios, outros métodos e a resposta, e os dois gastam do mesmo orçamento em voo de 64 kibibytes definido no padrão WHATWG Fetch.
Qual é a diferença entre sendBeacon vs fetch keepalive?
A diferença prática entre sendBeacon vs fetch keepalive é o controle: navigator.sendBeacon() enfileira um POST que sobrevive à página mas não devolve nada além de true ou false, enquanto fetch() com keepalive: true deixa você escolher o método, definir cabeçalhos próprios e ler a resposta do servidor. A especificação Beacon do W3C é explícita: "The sendBeacon() method does not provide ability to customize the request method, provide custom request headers, or change other processing properties of the request and response. Applications that require non-default settings for such requests should use the [FETCH] API with keepalive set to true." Os dois rodam sobre a mesma maquinaria de fetch por baixo, então um único limite de tamanho governa ambos.
Quantos dados sendBeacon vs fetch keepalive conseguem enviar?
Uma requisição keepalive carrega no máximo 64 kibibytes de corpo, ou 65.536 bytes. O padrão WHATWG Fetch aplica o teto dentro de HTTP-network-or-cache fetch, antes de a requisição chegar à rede:
If contentLength is non-null and httpRequest's keepalive is true, then:
- Let inflightKeepaliveBytes be 0.
- Let group be httpRequest's client's fetch group.
- Let inflightRecords be the set of fetch records in group whose request's keepalive is true and done flag is unset.
- For each fetchRecord of inflightRecords:
- Let inflightRequest be fetchRecord's request.
- Increment inflightKeepaliveBytes by inflightRequest's body's length.
- If the sum of contentLength and inflightKeepaliveBytes is greater than 64 kibibytes, then return a network error.
A nota da especificação explica o número: "The above limit ensures that requests that are allowed to outlive the environment settings object and contain a body, have a bounded size and are not allowed to stay alive indefinitely." Meça a carga serializada em bytes, não em caracteres, antes de confiar que ela cabe.
O limite de 64 KiB é compartilhado entre as requisições keepalive em voo?
O teto é compartilhado, não por chamada. O passo 5 acima soma contentLength com inflightKeepaliveBytes, o comprimento total do corpo de cada requisição keepalive inacabada do grupo de fetch. A especificação Beacon diz o mesmo do lado dela: "Requests initiated via the Beacon API automatically set the keepalive flag, and developers can similarly set the same flag manually when using the Fetch API. All requests with this flag set share the same in-flight quota restrictions that is enforced within the Fetch API." Uma chamada de sendBeacon() e uma chamada de fetch(..., { keepalive: true }) disputam um único orçamento.
A folga que sobra para a sua próxima chamada é:
remaining bytes = 65536 - (sum of body lengths of unfinished keepalive requests)
Digamos que um relatório de erro de 12.288 bytes e um lote de cliques de 8.192 bytes estejam os dois em voo. A folga é 65.536 menos 20.480, ou seja 45.056 bytes. Um resumo de sessão de 50.000 bytes empurra a soma para 70.480, então fetch() devolve um erro de rede e sendBeacon() devolve false. Junte cada carga de rastreamento de eventos em um único envio e essa conta para de morder.
Qual Content-Type o sendBeacon define para cada tipo de corpo?
sendBeacon nunca deixa você definir Content-Type na mão: o navegador o deduz do tipo de corpo que você passa, e esse valor decide se a requisição fica no modo no-cors ou dispara um preflight CORS. O modelo de processamento da especificação Beacon detalha a bifurcação:
If contentType is not null:
- Set corsMode to "cors".
- If contentType value is a CORS-safelisted request-header value for the
Content-Typeheader, set corsMode to "no-cors".- Append a
Content-Typeheader with value contentType to headerList.
O padrão Fetch define o teste de safelist para esse cabeçalho: "If mimeType's essence is not application/x-www-form-urlencoded, multipart/form-data, or text/plain, then return false." O Fetch também fixa o Content-Type que cada tipo de corpo produz:
| Corpo que você passa ao sendBeacon | Content-Type que o navegador define | Na safelist do CORS | Resultado cross-origin |
|---|---|---|---|
| ArrayBuffer, TypedArray, DataView | nenhum | nenhum cabeçalho enviado | no-cors, sem preflight |
| String | text/plain;charset=UTF-8 | Sim | no-cors, sem preflight |
| URLSearchParams | application/x-www-form-urlencoded;charset=UTF-8 | Sim | no-cors, sem preflight |
| FormData | multipart/form-data; boundary=... | Sim | no-cors, sem preflight |
Blob com tipo application/json | application/json | Não | modo CORS, preflight obrigatório |
A especificação Beacon enuncia a consequência: "If the request does not contain a payload, or the request Content-Type is a CORS-safelisted request-header, then the request mode is no-cors." Caso contrário, nas palavras da especificação, "a CORS preflight is made and the server needs to first allow such requests by returning the appropriate set of CORS headers: Access-Control-Allow-Credentials, Access-Control-Allow-Origin, Access-Control-Allow-Headers." Um preflight dobra as idas e voltas em uma página que já está saindo, então mande o JSON como string simples e faça o parse no servidor.
![]()
Por que um beacon deve disparar no pagehide em vez do unload?
Dispare o beacon primeiro no visibilitychange e depois no pagehide, e deixe o unload em paz, porque unload quebra o cache de voltar/avançar. A página do sendBeacon no MDN diz isso sem rodeios: "the unload event is incompatible with the back/forward cache (bfcache) implemented in modern browsers. Some browsers, such as Firefox, handle this incompatibility by excluding pages from the bfcache if they contain unload handlers, thus hurting performance." O MDN recomenda pagehide como alternativa para navegadores sem visibilitychange: "Like beforeunload and unload, this event is not reliably fired, especially on mobile. However, it is compatible with the bfcache."
A especificação Beacon acrescenta o motivo do celular: "Developers should avoid relying on unload event because it will not fire whenever a page is in a background state (i.e. visibilityState equal to hidden) and the process is terminated by the mobile OS." A comparação completa entre os eventos de saída está em beforeunload vs pagehide.
Painel em tempo real
Rastreamento de metas
Rastreamento sem cookies
Como escrever uma chamada de sendBeacon que sobrevive ao unload?
Passe uma string simples, confira o booleano devolvido e limpe o buffer só depois que o navegador aceitar a fila:
const endpoint = "https://collect.example.com/session";
const buffer = { sessionId: crypto.randomUUID(), events: [] };
function flush() {
if (buffer.events.length === 0) return;
const body = JSON.stringify(buffer);
if (navigator.sendBeacon(endpoint, body)) {
buffer.events.length = 0;
}
}
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") flush();
});
window.addEventListener("pagehide", flush);O corpo em string produz text/plain;charset=UTF-8, que está na safelist, então o POST cross-origin pula o preflight. Um retorno false significa que a carga mais tudo que está em voo passou de 65.536 bytes, e o buffer sobrevive para a próxima tentativa.
Como escrever a mesma chamada com fetch keepalive?
Defina keepalive: true, acrescente os cabeçalhos que o sendBeacon não carrega e aceite que a promise só resolve se a página viver o bastante para ver isso:
window.addEventListener("pagehide", () => {
fetch("https://collect.example.com/session", {
method: "POST",
keepalive: true,
headers: {
"Content-Type": "application/json",
"X-Api-Key": apiKey,
},
body: JSON.stringify(buffer),
}).catch(() => {});
});Tanto Content-Type: application/json quanto o cabeçalho próprio X-Api-Key ficam fora da safelist do CORS, então uma chamada cross-origin aqui dispara um preflight OPTIONS e precisa de Access-Control-Allow-Headers no servidor. Uma restrição vale só para keepalive: o algoritmo extract-a-body do padrão Fetch lança um TypeError para um corpo ReadableStream quando a flag está ligada, então fazer streaming na saída está fora de questão. Esse momento de saída é o motivo de um script leve valer o que custa, e de o custo de performance do session replay aparecer no envio, não na gravação.
Qual dos dois usar?
Use sendBeacon para analytics de disparar e esquecer, e fetch keepalive sempre que um cabeçalho, um método ou a resposta importarem.
| Requisito | sendBeacon | fetch keepalive |
|---|---|---|
| Método HTTP | só POST | qualquer método |
| Cabeçalhos de requisição próprios | não suportado | suportado |
| Controle do Content-Type | deduzido do tipo de corpo | definido por você |
| Resposta do servidor | indisponível | lida pela promise |
| Tamanho do corpo | divide o orçamento de 64 KiB | divide o mesmo orçamento |
| Disponível em service workers | Não | Sim |
Passando de 64 kibibytes, nenhum dos dois funciona: faça amostragem da carga, divida ela, ou mova a agregação para o servidor, uma escolha coberta em analytics client-side e server-side.
![]()
Perguntas frequentes
Um retorno true do sendBeacon significa que o servidor recebeu os dados?
Não. A especificação Beacon diz que um retorno true "implies the browser has queued the data for transfer" e que "since the actual data transfer happens asynchronously, this method does not provide any information whether the data transfer has succeeded or not." Um retorno false é o único outro sinal, e quer dizer que a fila recusou a carga.
Posso definir um cabeçalho Authorization no sendBeacon?
Não. A especificação Beacon afirma que sendBeacon "does not provide ability to customize the request method, provide custom request headers, or change other processing properties of the request and response." Mova a credencial para o caminho da URL, para um cookie ou para o corpo, ou troque para fetch() com keepalive: true.
Por que meu Blob do sendBeacon dispara um preflight CORS?
Porque o type do Blob vira o Content-Type da requisição, e application/json não está na safelist do CORS. O padrão Fetch limita essa safelist a application/x-www-form-urlencoded, multipart/form-data e text/plain, então qualquer outra coisa força um preflight OPTIONS. Mande o JSON como string e a requisição continua em no-cors.
O limite de 64 KiB é por requisição ou por página?
Por grupo de fetch, sobre cada requisição keepalive inacabada dentro dele. O padrão Fetch soma o contentLength do corpo novo com inflightKeepaliveBytes e devolve um erro de rede acima de 64 kibibytes, então um beacon grande ainda em voo encolhe o espaço para o próximo.
O fetch keepalive funciona em um service worker?
Sim. O MDN lista a disponibilidade em service workers como uma vantagem de fetch() com keepalive sobre sendBeacon(), junto com métodos diferentes de POST, propriedades próprias de requisição e acesso à resposta.
O que acontece com uma requisição keepalive se o usuário fecha a aba na hora?
O navegador mantém a requisição viva além do documento que a iniciou, que é o propósito da flag. O padrão Fetch descreve keepalive como algo que permite "the request to outlive the environment settings object," e o teto existe para que essas requisições sobreviventes "have a bounded size and are not allowed to stay alive indefinitely." O que você perde é a resposta: a promise não tem página viva onde resolver.
O sendBeacon pode usar métodos além do POST?
O sendBeacon só envia POST, como mostra a tabela comparativa. O fetch com keepalive aceita qualquer método, então troque para fetch quando o endpoint esperar PUT, PATCH ou GET.
Por que um corpo em streaming falha no fetch keepalive?
O algoritmo extract-a-body do padrão Fetch lança um TypeError para um corpo ReadableStream sempre que keepalive está definido, então um stream não consegue sair da página ao fechar. Serialize o payload com JSON.stringify ou passe um corpo do tipo string.
Painel em tempo real
Rastreamento de metas
Rastreamento sem cookies
Devo ouvir pagehide e visibilitychange juntos, ou escolher um?
Dispare primeiro no visibilitychange, porque ele captura o momento em que a aba vai para segundo plano antes de a página realmente descarregar, e adicione pagehide como alternativa para navegadores em que visibilitychange não dispara de forma confiável. A função flush do exemplo do post registra os dois listeners e deixa o que disparar primeiro enviar o beacon.
O que fazer quando meu payload passa de 64 KiB?
Nem sendBeacon nem fetch keepalive conseguem carregar um corpo além de 65.536 bytes; a requisição retorna um erro de rede ou um resultado false assim que o orçamento compartilhado é ultrapassado. Faça amostragem do payload, divida-o em vários envios ou mova a agregação para o servidor.
Este artigo foi útil?
Diga-nos o que pensa!
Veja-nos mais no Google
Um clique marca a Flowsery como fonte preferida e nossos artigos passam a aparecer mais acima nas suas Principais notícias, no modo IA e nas visões gerais com IA.
Antes de ir...
Flowsery
Analytics orientado para receitas para o seu site
Rastreie cada visitante, fonte e conversão em tempo real. Simples, poderoso e sem cookies.
Painel em tempo real
Rastreamento de metas
Rastreamento sem cookies
Termos relacionados do glossário


Por que beforeunload vs pagehide decide se o analytics sobrevive
Comparar beforeunload vs pagehide mostra por que o celular pula beforeunload, bloqueia o bfcache e pagehide envia dados de forma confiável com sendBeacon.


Por que o CNAME cloaking não esconde mais rastreadores do Safari ou do Brave
Rastreadores usavam o CNAME cloaking para se passar por um subdomínio seu. Veja o truque de DNS, o limite de 7 dias do Safari e como Brave e uBlock o revelam.


Como a sincronização de cookies cruza IDs de anúncios entre sites
Empresas de ad tech usam a sincronização de cookies para trocar IDs por redirecionamentos e pixels. Veja o que Safari, Firefox e Chrome fazem contra ela.


Como criar métricas calculadas do GA4 dentro de uma cota de cinco vagas
O Google limita as métricas calculadas do GA4 a 5 por propriedade padrão. Veja a sintaxe da fórmula, as unidades, onde aparecem e o que ela não aceita.


Como configurar o agrupamento de conteúdo do GA4 com um parâmetro
Configure o agrupamento de conteúdo do GA4 com o parâmetro content_group no gtag ou no Tag Manager, leia a dimensão Content group e evite o (not set).


O que os User-Agent Client Hints enviam e o que o analytics ainda vê
No Chrome, os User-Agent Client Hints dividem os dados do navegador em cabeçalhos Sec-CH-UA. Veja entropia, Accept-CH, o UA reduzido e o que o analytics lê.
Artigos relacionados


Onde rage clicks vs dead clicks realmente se separam
A divisão entre rage clicks vs dead clicks é de causa, não de gravidade. Os limiares publicados por PostHog, Hotjar, FullStory, LogRocket e Clarity.


Onde os padrões da redação de PII no session replay deixam dados expostos
Os padrões da redação de PII no session replay variam muito: o rrweb mascara só senhas, o Sentry mascara todo o texto e o Clarity mascara números e e-mails.


O que o tráfego Unassigned no GA4 realmente significa
Um canal sem regra correspondente: o tráfego Unassigned no GA4 não é um valor ausente como (not set). As regras do Google explicam a diferença.