TL;DR, Respuesta rápida
8 min de lecturaLa distancia entre sendBeacon vs fetch keepalive está en el control, no en la entrega. navigator.sendBeacon() encola un POST que sobrevive a la página y devuelve solo true o false, con un Content-Type que el navegador deduce del tipo de cuerpo que le pasas. fetch() con keepalive en true añade cabeceras propias, otros métodos y la respuesta, y ambos gastan del mismo presupuesto en vuelo de 64 kibibytes definido en el estándar WHATWG Fetch.
¿Cuál es la diferencia entre sendBeacon vs fetch keepalive?
La diferencia práctica entre sendBeacon vs fetch keepalive es el control: navigator.sendBeacon() encola un POST que sobrevive a la página pero no devuelve nada salvo true o false, mientras que fetch() con keepalive: true te deja elegir el método, poner cabeceras propias y leer la respuesta del servidor. La especificación Beacon del W3C es 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." Ambos corren sobre la misma maquinaria de fetch por debajo, así que un solo límite de tamaño gobierna a los dos.
¿Cuántos datos pueden enviar sendBeacon vs fetch keepalive?
Una petición keepalive lleva como mucho 64 kibibytes de cuerpo, o 65.536 bytes. El estándar WHATWG Fetch aplica el tope dentro de HTTP-network-or-cache fetch, antes de que la petición llegue a la red:
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.
La nota de la especificación explica el 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." Mide la carga serializada en bytes, no en caracteres, antes de confiar en que cabe.
¿El límite de 64 KiB se comparte entre las peticiones keepalive en vuelo?
El tope es compartido, no por llamada. El paso 5 de arriba suma contentLength con inflightKeepaliveBytes, la longitud total del cuerpo de cada petición keepalive sin terminar del grupo de fetch. La especificación Beacon dice lo mismo desde su lado: "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." Una llamada a sendBeacon() y una llamada a fetch(..., { keepalive: true }) compiten por un único presupuesto.
El margen que te queda para la siguiente llamada es:
remaining bytes = 65536 - (sum of body lengths of unfinished keepalive requests)
Imagina un informe de errores de 12.288 bytes y un lote de clics de 8.192 bytes, los dos en vuelo. El margen es 65.536 menos 20.480, o sea 45.056 bytes. Un resumen de sesión de 50.000 bytes empuja la suma a 70.480, así que fetch() devuelve un error de red y sendBeacon() devuelve false. Agrupa cada carga de seguimiento de eventos en un solo envío y esa aritmética deja de morder.
¿Qué Content-Type asigna sendBeacon a cada tipo de cuerpo?
sendBeacon nunca te deja poner Content-Type a mano: el navegador lo deduce del tipo de cuerpo que le pasas, y ese valor decide si la petición se queda en modo no-cors o dispara un preflight CORS. El modelo de procesamiento de la especificación Beacon detalla la bifurcación:
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.
El estándar Fetch define la prueba de la safelist para esa cabecera: "If mimeType's essence is not application/x-www-form-urlencoded, multipart/form-data, or text/plain, then return false." Fetch también fija el Content-Type que produce cada tipo de cuerpo:
| Cuerpo que pasas a sendBeacon | Content-Type que pone el navegador | En la safelist de CORS | Resultado entre orígenes |
|---|---|---|---|
| ArrayBuffer, TypedArray, DataView | ninguno | no se envía cabecera | no-cors, sin preflight |
| String | text/plain;charset=UTF-8 | Sí | no-cors, sin preflight |
| URLSearchParams | application/x-www-form-urlencoded;charset=UTF-8 | Sí | no-cors, sin preflight |
| FormData | multipart/form-data; boundary=... | Sí | no-cors, sin preflight |
Blob con tipo application/json | application/json | No | modo CORS, preflight obligatorio |
La especificación Beacon enuncia la consecuencia: "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." En caso contrario, en palabras de la especificación, "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." Un preflight duplica los viajes de ida y vuelta en una página que ya se está descargando, así que envía el JSON como cadena simple y parséalo en el servidor.
![]()
¿Por qué un beacon debe dispararse en pagehide y no en unload?
Dispara el beacon primero en visibilitychange y en segundo lugar en pagehide, y deja unload en paz, porque unload rompe la caché atrás/adelante. La página de sendBeacon de MDN lo dice sin rodeos: "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." MDN recomienda pagehide como respaldo para navegadores sin visibilitychange: "Like beforeunload and unload, this event is not reliably fired, especially on mobile. However, it is compatible with the bfcache."
La especificación Beacon añade el motivo móvil: "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." La comparación completa entre los eventos de salida vive en beforeunload vs pagehide.
Panel en tiempo real
Seguimiento de objetivos
Rastreo sin cookies
¿Cómo escribes una llamada a sendBeacon que sobreviva a la descarga?
Pasa una cadena simple, comprueba el booleano que devuelve y vacía el búfer solo cuando el navegador acepte la cola:
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);El cuerpo de tipo cadena produce text/plain;charset=UTF-8, que está en la safelist, así que el POST entre orígenes se salta el preflight. Un false de vuelta significa que la carga más todo lo que hay en vuelo cruzó los 65.536 bytes, y el búfer sobrevive para el siguiente intento.
¿Cómo escribes esa misma llamada con fetch keepalive?
Pon keepalive: true, añade las cabeceras que sendBeacon no llevará y acepta que la promesa se resuelve solo si la página vive lo bastante para verlo:
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 como la cabecera propia X-Api-Key quedan fuera de la safelist de CORS, así que una llamada entre orígenes aquí dispara un preflight OPTIONS y necesita Access-Control-Allow-Headers en el servidor. Una restricción se aplica solo a keepalive: el algoritmo extract-a-body del estándar Fetch lanza un TypeError para un cuerpo ReadableStream cuando la bandera está puesta, así que hacer streaming en la salida queda descartado. Ese momento de salida es la razón por la que un script ligero se gana el sitio, y por la que la sobrecarga de rendimiento del session replay aparece en el envío, no en la grabación.
¿Cuál deberías usar?
Usa sendBeacon para analíticas de disparar y olvidar, y fetch keepalive siempre que importe una cabecera, un método o la respuesta.
| Requisito | sendBeacon | fetch keepalive |
|---|---|---|
| Método HTTP | solo POST | cualquier método |
| Cabeceras propias de petición | no admitidas | admitidas |
| Control del Content-Type | deducido del tipo de cuerpo | lo pones tú |
| Respuesta del servidor | no disponible | se lee en la promesa |
| Tamaño del cuerpo | comparte el presupuesto de 64 KiB | comparte ese mismo presupuesto |
| Disponible en service workers | No | Sí |
Pasados los 64 kibibytes no funciona ninguno: muestrea la carga, divídela o mueve la agregación al servidor, una decisión que cubre analítica del lado del cliente y del lado del servidor.
![]()
Preguntas frecuentes
¿Un valor de retorno true en sendBeacon significa que el servidor recibió los datos?
No. La especificación Beacon dice que un true de vuelta "implies the browser has queued the data for transfer" y que "since the actual data transfer happens asynchronously, this method does not provide any information whether the data transfer has succeeded or not." Un false es la única otra señal, y quiere decir que la cola rechazó la carga.
¿Puedo poner una cabecera Authorization en sendBeacon?
No. La especificación 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." Mueve la credencial a la ruta de la URL, a una cookie o al cuerpo, o cambia a fetch() con keepalive: true.
¿Por qué mi Blob de sendBeacon dispara un preflight CORS?
Porque el type del Blob se convierte en el Content-Type de la petición, y application/json no está en la safelist de CORS. El estándar Fetch limita esa safelist a application/x-www-form-urlencoded, multipart/form-data y text/plain, así que cualquier otra cosa fuerza un preflight OPTIONS. Envía el JSON como cadena y la petición se queda en no-cors.
¿El límite de 64 KiB es por petición o por página?
Por grupo de fetch, sobre cada petición keepalive sin terminar que haya dentro. El estándar Fetch suma el contentLength del cuerpo nuevo con inflightKeepaliveBytes y devuelve un error de red pasados los 64 kibibytes, así que un beacon grande todavía en vuelo encoge el hueco para el siguiente.
¿Funciona fetch keepalive en un service worker?
Sí. MDN lista la disponibilidad en service workers como una ventaja de fetch() con keepalive frente a sendBeacon(), junto a los métodos distintos de POST, las propiedades propias de la petición y el acceso a la respuesta.
¿Qué le pasa a una petición keepalive si el usuario cierra la pestaña de inmediato?
El navegador mantiene viva la petición más allá del documento que la inició, que es el propósito de la bandera. El estándar Fetch describe keepalive como algo que permite "the request to outlive the environment settings object," y el tope existe para que esas peticiones supervivientes "have a bounded size and are not allowed to stay alive indefinitely." Lo que pierdes es la respuesta: la promesa no tiene página viva en la que resolverse.
¿Puede sendBeacon usar métodos distintos de POST?
sendBeacon solo envía POST, como muestra la tabla comparativa. fetch con keepalive acepta cualquier método, así que cambia a fetch cuando el endpoint espere PUT, PATCH o GET.
¿Por qué falla un cuerpo en streaming con fetch keepalive?
El algoritmo extract-a-body del estándar Fetch lanza un TypeError para un cuerpo ReadableStream siempre que keepalive esté activo, así que un stream no puede salir de la página al cerrarla. Serializa la carga con JSON.stringify o pasa un cuerpo de tipo string.
Panel en tiempo real
Seguimiento de objetivos
Rastreo sin cookies
¿Debo escuchar pagehide y visibilitychange juntos, o elegir uno solo?
Dispara primero en visibilitychange, porque captura el momento en que una pestaña pasa a segundo plano antes de que la página se descargue, y añade pagehide como respaldo para navegadores donde visibilitychange no se dispara de forma fiable. La función flush del ejemplo del artículo añade ambos listeners y deja que el que se dispare primero envíe el beacon.
¿Qué hago cuando mi carga supera los 64 KiB?
Ni sendBeacon ni fetch keepalive pueden llevar un cuerpo más allá de 65.536 bytes; la petición devuelve un error de red o un resultado false en cuanto se supera el presupuesto compartido. Muestrea la carga, divídela en varios envíos o traslada la agregación al servidor.
¿Te resultó útil este artículo?
¡Cuéntanos qué opinas!
Vernos más en Google
Un clic marca Flowsery como fuente preferida y nuestros artículos aparecen más arriba en tus Noticias destacadas, el modo IA y los resúmenes con IA.
Antes de irte...
Flowsery
Analítica orientada a ingresos para tu sitio web
Rastrea cada visitante, fuente y conversión en tiempo real. Simple, potente y sin cookies.
Panel en tiempo real
Seguimiento de objetivos
Rastreo sin cookies
Términos relacionados del glosario


Por qué beforeunload vs pagehide decide si tus analíticas sobreviven
Comparar beforeunload vs pagehide muestra por qué el móvil se salta beforeunload, bloquea el bfcache y pagehide envía datos de forma fiable con sendBeacon.


Por qué el CNAME cloaking ya no oculta rastreadores a Safari ni a Brave
Los rastreadores usaban el CNAME cloaking para pasar por tu subdominio. Mira el truco DNS, el límite de 7 días de Safari y cómo Brave y uBlock lo destapan.


Cómo la sincronización de cookies empareja IDs publicitarios entre sitios
El ad-tech usa la sincronización de cookies para intercambiar IDs con redirecciones y píxeles. Mira qué hacen Safari, Firefox y Chrome al respecto.


Cómo crear métricas calculadas de GA4 con una cuota de cinco huecos
Google limita las métricas calculadas de GA4 a 5 por propiedad estándar. Aprende la sintaxis, las unidades, dónde aparecen y qué no admite una fórmula.


Cómo configurar la agrupación de contenido de GA4 con un solo parámetro
Configura la agrupación de contenido de GA4 con el parámetro content_group en gtag o Tag Manager, lee la dimensión Content group y evita filas (not set).


Qué envían los user agent client hints y qué sigue viendo la analítica
Con los user agent client hints, Chrome reparte los datos del navegador en encabezados Sec-CH-UA. Mira baja y alta entropía, Accept-CH y qué lee la analítica.
Artículos relacionados


En qué se diferencian de verdad rage clicks vs dead clicks
La distinción rage clicks vs dead clicks va de causa, no de gravedad. Umbrales publicados por PostHog, Hotjar, FullStory, LogRocket y Clarity.


Dónde los valores por defecto de la redacción de PII en session replay dejan datos expuestos
Los valores por defecto de la redacción de PII en session replay varían: rrweb solo enmascara contraseñas, Sentry todo el texto, Clarity números y correos.


Qué significa realmente el tráfico Unassigned en GA4
Un canal sin regla que lo recoja: el tráfico Unassigned en GA4 no es un valor ausente como (not set). Las reglas de Google explican la diferencia.