Lección 2 de 413 min
Tiempo real y eventos
Webhooks, Server-Sent Events, WebSockets, MQTT y GraphQL Subscriptions: cómo enterarse de algo en cuanto ocurre.
Pides comida a domicilio y la app te enseña el estado del pedido: «preparando», «en reparto», «entregado». ¿Cómo se entera? Podría preguntar al servidor cada pocos segundos, pero casi siempre la respuesta sería «nada nuevo». Lo eficiente es que quien tiene la noticia avise en cuanto ocurre.
En esta lección ves las formas de conseguirlo: webhooks entre servidores, Server-Sent Events y WebSockets hacia el navegador, MQTT para dispositivos y las suscripciones de GraphQL. En la lección «Tiempo real y estilos de API» del módulo 1 viste WebSocket por dentro; aquí te centras en cuándo elegir cada opción.
Preguntar o que te avisen
Polling frente a push
Preguntar cada poco si hay algo nuevo, o que te lo envíen cuando ocurra.
- Variante:
- Long polling
Con polling, el cliente pregunta cada cierto tiempo: «¿ha cambiado el pedido?». Es lo más sencillo y funciona en cualquier sitio, pero la mayoría de las respuestas no traen nada y la noticia puede llegar con todo el retraso del intervalo. El long polling lo mejora: el servidor retiene la petición hasta que hay novedades o se agota un tiempo, y el cliente vuelve a preguntar en cuanto recibe la respuesta.
Con push, quien tiene la noticia la envía en cuanto se produce. Según quién hable con quién, eso es un webhook (de servidor a servidor), Server-Sent Events o WebSocket (del servidor al navegador) o un broker MQTT (hacia dispositivos).
Úsalo cuando
El polling basta cuando los cambios son poco frecuentes o un retraso de unos segundos no importa, como comprobar si ha terminado de generarse un informe.
Ten en cuenta
Con muchos clientes, el polling multiplica la carga: mil usuarios que preguntan cada 5 segundos son 200 peticiones por segundo, casi todas inútiles. El push, a cambio, exige mantener conexiones abiertas o tener una URL pública que reciba los avisos.
// Preguntar cada 5 segundos
const temporizador = setInterval(async () => {
const respuesta = await fetch("/api/pedidos/381/estado");
const { estado } = await respuesta.json();
pintarEstado(estado); // casi siempre, el mismo que antes
if (estado === "entregado") clearInterval(temporizador);
}, 5000);Webhooks
Un POST que otro servidor te envía en cuanto ocurre algo.
- Dirección:
- Servidor → servidor
- Transporte:
- HTTP POST
Un webhook es una petición HTTP POST que un servicio envía a una URL que tú le has dado cuando sucede un evento: un pago completado, un push a un repositorio, una factura emitida. Es comunicación de servidor a servidor, así que tu endpoint tiene que ser accesible desde internet. Pasarelas de pago, GitHub y GitLab funcionan así.
Como cualquiera podría enviar un POST a esa URL, el proveedor firma cada envío: calcula un HMAC del cuerpo con un secreto que solo conocen el proveedor y tú, y lo manda en una cabecera. Tú repites el cálculo y, si no coincide, rechazas la petición. Muchos proveedores incluyen también una marca de tiempo para que no se pueda reenviar un mensaje antiguo.
Si tu servidor no responde con un 2xx a tiempo, el proveedor reintenta, normalmente con esperas crecientes. Así que el mismo evento puede llegarte dos veces, y tu endpoint tiene que ser idempotente: procesar dos veces el mismo evento debe dejar todo igual que procesarlo una.
Úsalo cuando
Para enterarte de lo que pasa en un servicio externo (pagos, repositorios, CRM), o para avisar tú a los sistemas de tus clientes sin que tengan que consultar tu API cada minuto.
Ten en cuenta
Responde rápido y deja el trabajo pesado para una cola. No des por hecho el orden: un pedido.actualizado puede llegar antes que su pedido.creado. Y para probarlos en local necesitas un túnel que exponga tu equipo a internet, o la herramienta de línea de comandos del proveedor.
import crypto from "node:crypto";
// express.raw: la firma se calcula sobre el cuerpo tal cual llegó
app.post("/webhooks/pagos", express.raw({ type: "application/json" }), (req, res) => {
const esperada = crypto.createHmac("sha256", process.env.WEBHOOK_SECRETO)
.update(req.body).digest("hex");
const recibida = req.get("X-Firma") ?? "";
const valida = recibida.length === esperada.length &&
crypto.timingSafeEqual(Buffer.from(recibida), Buffer.from(esperada));
if (!valida) return res.sendStatus(401);
const evento = JSON.parse(req.body);
if (yaProcesado(evento.id)) return res.sendStatus(200); // un reintento repetido
encolar(evento); // el trabajo pesado, después
res.sendStatus(200);
});Atención
Compara siempre las firmas con una función de tiempo constante, como crypto.timingSafeEqual. Con un === normal, la comparación termina antes cuanto antes difieren los textos, y midiendo esos tiempos un atacante podría ir adivinando la firma.
Canales abiertos con el navegador
Para que el servidor avise al navegador, alguien tiene que mantener una conexión abierta. Las tres opciones que siguen lo hacen de formas distintas: en un solo sentido, en los dos o con el modelo de GraphQL.
Server-Sent Events (SSE)
Una respuesta HTTP que no termina: el servidor va escribiendo eventos.
- Dirección:
- Servidor → navegador
- Tipo MIME:
- text/event-stream
El navegador abre la conexión con new EventSource(url) y el servidor responde con Content-Type: text/event-stream, pero sin cerrar la respuesta: va escribiendo eventos como líneas de texto (event:, data:, id:), separados por una línea en blanco. Es unidireccional: del servidor al navegador.
Su gran ventaja es la reconexión automática: si se corta, el navegador vuelve a conectar solo y envía la cabecera Last-Event-ID con el último evento recibido, para que el servidor le mande lo que se perdió. Y como es HTTP normal, funciona con la autenticación por cookies y con la infraestructura que ya tienes; eso sí, algunos proxies acumulan la respuesta antes de reenviarla, y hay que desactivarles ese búfer.
Úsalo cuando
Para notificaciones, barras de progreso, paneles en directo y respuestas que se generan poco a poco: las respuestas de los chats de IA que se escriben palabra a palabra suelen llegar por SSE.
Ten en cuenta
Solo envía texto, y en un solo sentido: para mandar algo al servidor usas un fetch aparte. EventSource no permite añadir cabeceras propias como Authorization. Y con HTTP/1.1 el navegador limita las conexiones simultáneas por dominio, unas seis, compartidas entre pestañas; con HTTP/2 ese límite deja de ser un problema.
const fuente = new EventSource("/api/pedidos/381/eventos");
fuente.addEventListener("estado", (evento) => {
const { estado } = JSON.parse(evento.data);
pintarEstado(estado);
});
// Si la conexión se cae, el navegador reconecta solo
/* Lo que escribe el servidor (Content-Type: text/event-stream):
id: 12
event: estado
data: {"estado":"en reparto"}
*/WebSockets
Una conexión permanente por la que ambos lados envían mensajes cuando quieren.
- Dirección:
- Bidireccional
- Estándar:
- RFC 6455
- Esquemas:
- ws://, wss://
Una conexión WebSocket empieza como una petición HTTP con la cabecera Upgrade: websocket; el servidor responde 101 Switching Protocols y, desde ahí, la misma conexión transporta mensajes en los dos sentidos en cualquier momento, con muy poca sobrecarga por mensaje. Usa ws:// o, cifrado con TLS, wss://, que es el que debes usar en producción.
WebSocket solo transporta mensajes: tú decides su formato, cómo distinguir un mensaje de chat de un aviso de «escribiendo…» y cómo reconectar si se corta. A diferencia de EventSource, la reconexión no es automática.
Úsalo cuando
Cuando el navegador también envía mucho y a menudo: chats, juegos multijugador, edición colaborativa o pizarras compartidas.
Ten en cuenta
Cada servidor mantiene sus conexiones abiertas: si tienes varios, un mensaje para usuarios conectados a otro servidor necesita un canal interno de publicación, por ejemplo con Redis. Añade latidos (ping) para detectar conexiones muertas, y reconecta con esperas crecientes para no saturar el servidor cuando vuelva.
function conectar() {
const socket = new WebSocket("wss://chat.ejemplo.com/salas/general");
socket.addEventListener("open", () => {
socket.send(JSON.stringify({ tipo: "unirse", usuario: "ana" }));
});
socket.addEventListener("message", (evento) => {
const mensaje = JSON.parse(evento.data);
if (mensaje.tipo === "chat") mostrarMensaje(mensaje);
});
// No reconecta solo: lo programas tú
socket.addEventListener("close", () => setTimeout(conectar, 2000));
}
conectar();GraphQL Subscriptions
El tiempo real de GraphQL: te suscribes con una consulta y recibes eventos con su forma.
- Transporte habitual:
- WebSocket (graphql-ws)
- Alternativa:
- SSE
Además de consultas y mutaciones, GraphQL tiene un tercer tipo de operación: subscription. El cliente se suscribe eligiendo los campos que quiere, como en una consulta, y cada vez que ocurre el evento el servidor le envía un resultado con exactamente esa forma.
La especificación de GraphQL no dice por dónde viajan. Lo más común es un WebSocket con el protocolo graphql-ws, implementado por la biblioteca del mismo nombre; también hay servidores que las envían por SSE.
Úsalo cuando
Cuando tu app ya usa GraphQL y quieres que los datos en tiempo real lleguen con los mismos tipos, la misma validación y la misma selección de campos que el resto.
Ten en cuenta
Hay dos protocolos sobre WebSocket con nombres confusos: el antiguo de la biblioteca subscriptions-transport-ws, ya abandonada, y el de graphql-ws. Cliente y servidor tienen que hablar el mismo. Y si solo necesitas un aviso sencillo, SSE puede ser mucho más simple de mantener.
import { createClient } from "graphql-ws";
const cliente = createClient({ url: "wss://api.ejemplo.com/graphql" });
cliente.subscribe(
{
query: `subscription {
pedidoActualizado(id: 381) { estado repartidor { nombre } }
}`,
},
{
next: ({ data }) => pintarEstado(data.pedidoActualizado.estado),
error: (error) => console.error(error),
complete: () => console.log("Suscripción terminada"),
},
);Publicar y suscribirse: MQTT
Con WebSocket, cada cliente habla con tu servidor. En el modelo de publicación y suscripción (pub/sub), en cambio, nadie habla directamente con nadie: unos publican mensajes en un tema, otros se suscriben a los temas que les interesan y un intermediario, el broker, reparte cada mensaje. Quien publica no sabe quién escucha. Verás este modelo a fondo en el módulo de caché, colas y mensajería.
MQTT
Pub/sub ligero para dispositivos con poca batería y redes poco fiables.
- Estándar:
- OASIS
- Puertos:
- 1883; 8883 con TLS
- Modelo:
- Pub/sub con broker
MQTT es un protocolo de publicación y suscripción muy ligero, pensado para el internet de las cosas (IoT): sensores, enchufes inteligentes, vehículos. Cada dispositivo mantiene una conexión con un broker, como Mosquitto, EMQX o el servicio IoT de un proveedor cloud, y publica en temas jerárquicos separados por /, como casa/salon/temperatura.
Las suscripciones admiten comodines: + sustituye un nivel (casa/+/temperatura recibe el salón y la cocina) y #, al final, todos los niveles que siguen (casa/#). Cada mensaje elige su QoS: 0, como mucho una vez, sin confirmación; 1, al menos una vez, aunque puede duplicarse; y 2, exactamente una vez, con un intercambio en cuatro pasos más lento.
Un mensaje retenido (retained) se queda guardado en el broker como el último valor del tema, y cualquier suscriptor nuevo lo recibe nada más suscribirse, sin esperar a la siguiente lectura. Y el último deseo (Last Will) es un mensaje que el broker publica por ti si te desconectas de golpe: «el sensor 12 está desconectado».
Úsalo cuando
Para muchos dispositivos que envían lecturas pequeñas y frecuentes por redes inestables, o para dar órdenes a dispositivos que no siempre están conectados.
Ten en cuenta
El broker es una pieza crítica: si cae, nadie se entera de nada. Protege los temas con autenticación y permisos por tema, y usa TLS. Desde el navegador no puedes abrir una conexión MQTT directa, pero muchos brokers aceptan MQTT sobre WebSocket.
import mqtt from "mqtt";
const cliente = mqtt.connect("mqtts://broker.ejemplo.com:8883");
cliente.on("connect", () => {
// +: un nivel cualquiera; #: todos los niveles que siguen
cliente.subscribe("casa/+/temperatura", { qos: 1 });
});
cliente.on("message", (tema, mensaje) => {
console.log(`${tema}: ${mensaje.toString()} °C`);
});
// Un sensor publica; retain hace que el broker guarde el último valor
cliente.publish("casa/salon/temperatura", "21.5", { qos: 1, retain: true });Nota
QoS 2 no es «mejor» que QoS 1 sin más: cuesta más mensajes y más latencia. Lo habitual es usar QoS 1 y hacer que quien recibe sea idempotente, igual que con los webhooks.
Interactivo
Cómo enterarse de algo en cuanto ocurre
Activa o desactiva opciones para compararlas cara a cara.
| Característica | Server-Sent Events | WebSockets | Webhooks |
|---|---|---|---|
| Dirección | Servidor → navegador | En los dos sentidos | Servidor → servidor |
| Quién abre la conexión | El navegador, una vez | El navegador, con un Upgrade | El proveedor, en cada evento |
| Transporte | HTTP con text/event-stream | WebSocket sobre TCP | HTTP POST |
| En el navegador | Sí, con EventSource | Sí, con WebSocket | No: el receptor es un servidor |
| Si se corta | Reconecta solo, con Last-Event-ID | Reconectas tú | El proveedor reintenta |
| Uso típico | Notificaciones, progreso, texto que llega poco a poco | Chat, juegos, edición colaborativa | Pagos, repositorios, integraciones |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Tu tienda cobra con una pasarela de pago externa. Necesitas marcar el pedido como pagado cuando se complete el cobro, aunque el cliente cierre la pestaña antes.
Para recordar
- El polling es lo más simple, pero desperdicia peticiones y llega tarde; con push, quien tiene la noticia avisa en cuanto ocurre.
- Un webhook es un
POSTde servidor a servidor: verifica su firma HMAC y procésalo de forma idempotente, porque los reintentos traen duplicados. - Para enviar del servidor al navegador, SSE es más sencillo que WebSocket y reconecta solo; elige WebSocket cuando el canal tenga que ir en los dos sentidos.
- MQTT conecta dispositivos a través de un broker con temas, comodines y tres niveles de QoS; los mensajes retenidos dan el último valor al instante.
- Las suscripciones de GraphQL llevan el modelo de GraphQL al tiempo real, casi siempre sobre WebSocket con el protocolo graphql-ws.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.La pasarela de pago te envía dos veces el mismo webhook
pago.completadoporque la primera vez tu servidor tardó demasiado en responder. ¿Qué debería hacer tu endpoint?2.¿Para qué sirve la firma HMAC que acompaña a un webhook?
3.Necesitas que el servidor envíe actualizaciones al navegador y que la conexión se recupere sola si se corta. El navegador no necesita enviar nada por ese canal. ¿Qué es lo más sencillo?
4.En MQTT, ¿qué temas recibe una suscripción a
casa/+/temperatura?5.Un sensor publica sus lecturas con QoS 1. ¿Qué garantiza ese nivel?