Eventos enviados por el servidor en JavaScript (EventSource)
Aprende los Server-Sent Events de JavaScript con la API EventSource — recibe un flujo unidireccional de actualizaciones del servidor sobre HTTP, con reconexión automática y eventos con nombre.
Los Server-Sent Events (SSE) son una forma estándar de que un servidor envíe un flujo continuo y unidireccional de actualizaciones de texto al navegador a través de una única conexión HTTP persistente. En lugar de que el cliente sondee repetidamente un endpoint con fetch preguntando "¿hay algo nuevo?", la conexión permanece abierta y el servidor escribe mensajes cada vez que tiene algo que enviar. El navegador expone esto mediante la interfaz integrada EventSource, por lo que no se necesita ninguna librería externa para consumir un flujo.
SSE destaca cuando las actualizaciones fluyen en una sola dirección — del servidor al cliente. Los feeds de noticias en vivo, las notificaciones, las barras de progreso de compilación o subida, los paneles de monitoreo y los tickers de bolsa son todos casos de uso ideales.
Recibir un flujo con EventSource
Abrir un flujo requiere una sola línea. Se crea un EventSource apuntando a un endpoint del servidor y luego se adjuntan manejadores para los eventos que dispara.
const es = new EventSource('/events');
es.onmessage = (event) => {
console.log('New message:', event.data);
};
es.onopen = () => {
console.log('Connection opened');
};
es.onerror = (error) => {
console.log('Connection error:', error);
};Vale la pena destacar algunas cosas:
- La conexión se abre en cuanto se construye el
EventSource. onmessagese dispara para cada mensaje predeterminado (sin nombre) que envía el servidor. El contenido es siempre un string, disponible enevent.data.onopense dispara una vez que la conexión está establecida (y de nuevo tras una reconexión).onerrorse dispara cuando la conexión se cae o falla. A diferencia de unfetchfallido, esto no significa necesariamente que se deba abandonar — el navegador generalmente intentará reconectarse por sí solo (más sobre esto a continuación).
Aquí hay un ejemplo un poco más completo que analiza cargas JSON y actualiza la página:
const es = new EventSource('/notifications');
es.onmessage = (event) => {
const data = JSON.parse(event.data);
const li = document.createElement('li');
li.textContent = data.message;
document.querySelector('#feed').append(li);
};El formato de transmisión
El servidor responde con el tipo de contenido text/event-stream y escribe texto UTF-8 plano en un formato simple basado en líneas. Cada mensaje es un bloque de líneas campo: valor, y una línea en blanco marca el fin de un mensaje.
data: Hello there
data: First line
data: Second line
event: priceUpdate
data: {"symbol":"ACME","price":42.10}
id: 42
data: This message has an id
retry: 10000
data: Reconnect after 10 seconds next timeLos campos reconocidos son:
data:— la carga del mensaje. Si un mensaje tiene varias líneasdata:, se unen con un salto de línea (\n) antes de entregarse comoevent.data.event:— un nombre de evento opcional. Sin él, el mensaje se entrega aonmessage.id:— un identificador opcional que el navegador recuerda para la reconexión.retry:— el retardo de reconexión en milisegundos.
Las líneas que comienzan con dos puntos (:) son comentarios y se ignoran — los servidores suelen enviarlos como pings de mantenimiento de conexión.
Eventos con nombre
Por defecto, cada mensaje va a onmessage. Un servidor puede en cambio etiquetar un mensaje con un campo event:, y el cliente escucha ese nombre específico con addEventListener. Esto permite enrutar diferentes tipos de actualizaciones a distintos manejadores sobre la misma conexión.
Dado un flujo como este:
event: priceUpdate
data: {"symbol":"ACME","price":42.10}
event: newsAlert
data: Markets closed early today
data: a plain default messageSe configuraría el cliente de la siguiente manera:
const es = new EventSource('/market');
es.addEventListener('priceUpdate', (event) => {
const { symbol, price } = JSON.parse(event.data);
console.log(`${symbol} is now ${price}`);
});
es.addEventListener('newsAlert', (event) => {
console.log('News:', event.data);
});
// Messages with no `event:` field still land here.
es.onmessage = (event) => {
console.log('Default message:', event.data);
};Reconexión automática
Esta es la característica que distingue a SSE de implementar manualmente la transmisión con fetch. Si la conexión se cae — una red inestable, un reinicio del servidor, un timeout del proxy — el navegador se reconecta automáticamente después de una breve pausa. No es necesario escribir ningún bucle de reintento.
Para hacer que la reanudación sea confiable, el navegador lleva un seguimiento del último id: recibido. Al reconectarse, envía ese valor de vuelta en una cabecera de solicitud Last-Event-ID, para que el servidor pueda retomar exactamente donde se dejó el flujo en lugar de repetirlo todo.
GET /events HTTP/1.1
Last-Event-ID: 42
Accept: text/event-streamEl servidor también puede ajustar el retardo antes de la próxima reconexión enviando un campo retry: (en milisegundos):
retry: 5000
data: The browser will wait 5 seconds before reconnecting if droppedLa reconexión automática solo ocurre mientras la conexión se considera recuperable. Si el servidor responde a una reconexión con un error HTTP como 204 No Content o un estado 4xx, el navegador trata el flujo como finalizado y deja de intentarlo.
Estado de la conexión y cierre
Un EventSource expone su estado actual mediante la propiedad readyState, que coincide con tres constantes:
EventSource.CONNECTING(0) — conectando o esperando para reconectarse.EventSource.OPEN(1) — conectado y recibiendo.EventSource.CLOSED(2) — cerrado y no se reconectará.
Cuando ya no se necesita el flujo, se llama a es.close(). Esto cierra la conexión inmediatamente y — de forma importante — el navegador no se reconectará después.
const es = new EventSource('/events');
// Stop listening after 30 seconds.
setTimeout(() => {
es.close();
console.log('readyState is now', es.readyState); // 2 (CLOSED)
}, 30000);Solicitudes de origen cruzado y credenciales
Por defecto, un EventSource sigue la política del mismo origen. Para transmitir desde un origen diferente, el servidor debe enviar las cabeceras CORS apropiadas (como Access-Control-Allow-Origin).
Las cookies no se envían en solicitudes de origen cruzado a menos que se opte por ello. Se pasa la opción withCredentials y hay que asegurarse de que el servidor permita credenciales en su configuración CORS:
const es = new EventSource('https://api.example.com/events', {
withCredentials: true,
});Una nota sobre el lado del servidor
SSE es principalmente una característica del navegador, pero el servidor debe cooperar. Sea cual sea el lenguaje que se use, la receta es la misma: responder con Content-Type: text/event-stream, mantener la conexión abierta en lugar de finalizar la respuesta, y escribir fragmentos con formato de evento a medida que los datos estén disponibles.
Aquí hay un ejemplo mínimo de Node.js para mostrar su estructura:
import http from 'node:http';
http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
let id = 0;
const timer = setInterval(() => {
id += 1;
res.write(`id: ${id}\n`);
res.write(`data: The time is ${new Date().toISOString()}\n\n`);
}, 1000);
// Stop the timer when the client disconnects.
req.on('close', () => clearInterval(timer));
}).listen(3000);Cada fragmento termina con una línea en blanco (\n\n) para completar el mensaje. El cliente conectado con new EventSource('http://localhost:3000') recibiría una actualización por segundo.
Limitaciones
SSE solo transporta texto (UTF-8) — no existe un tipo de trama binaria, por lo que no se puede transmitir bytes sin procesar como imágenes o audio sin codificarlos primero. También es estrictamente unidireccional: para enviar datos al servidor aún se necesita una solicitud normal mediante fetch o XMLHttpRequest, o una conexión WebSocket de dúplex completo.
Otro límite práctico: sobre HTTP/1.1, los navegadores limitan el número de conexiones simultáneas a un mismo origen a aproximadamente seis. Como cada EventSource mantiene una conexión abierta, abrir muchas pestañas al mismo sitio puede agotar ese presupuesto. Esto es mucho menos preocupante sobre HTTP/2, donde muchos flujos se multiplexan en una sola conexión.
SSE vs. WebSockets
SSE y WebSockets ambos entregan datos en tiempo real, pero resuelven problemas diferentes.
| Server-Sent Events | WebSockets | |
|---|---|---|
| Dirección | Unidireccional (servidor → cliente) | Bidireccional (dúplex completo) |
| Protocolo | HTTP plano | Actualización a ws:// / wss:// |
| Datos | Solo texto (UTF-8) | Texto y binario |
| Reconexión | Automática, integrada | Implementar manualmente |
| Ideal para | Feeds, notificaciones, progreso | Chat, juegos, edición colaborativa |
Usa SSE cuando el navegador solo necesita escuchar — notificaciones, feeds en vivo, paneles de control, actualizaciones de progreso. Su reconexión automática, transporte HTTP plano y pequeña API de cliente lo convierten en la herramienta más sencilla para el trabajo. Usa WebSockets cuando el cliente también necesite enviar con frecuencia, cuando se necesiten datos binarios, o cuando la latencia por mensaje importe, como en apps de chat y juegos multijugador.
Si los datos ya residen detrás de un flujo asíncrono basado en promesas, combinar los manejadores de SSE con async/await para cualquier solicitud de seguimiento (por ejemplo, obtener detalles completos cuando llega una notificación ligera) mantiene el código limpio y legible.