¿Cómo diseñas un endpoint de API para una operación de larga duración (por ejemplo, procesamiento de video) — síncrono, polling o webhooks?
Hacer que el cliente espere con una conexión abierta durante una transcodificación de video de dos minutos es la receta perfecta para timeouts del gateway — la solución es aceptar el trabajo de inmediato y dejar que el cliente vuelva a preguntar.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo diseñas un endpoint de API para una operación de larga duración (por ejemplo, procesamiento de video) — síncrono, polling o webhooks?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Por qué lo síncrono no funciona aquí
Cada capa entre un cliente y tu aplicación tiene un límite de timeout: navegadores, librerías HTTP, balanceadores de carga y API gateways suelen cortar una petición en algún punto entre 30 y 120 segundos. Una transcodificación de video, la exportación de un reporte grande o una importación masiva de datos fácilmente pueden tardar minutos. Si intentas mantener la conexión HTTP abierta durante todo ese tiempo, estás apostando a que ninguno de esos timeouts intermedios se dispare, estás ocupando un hilo o worker del servidor durante todo el trabajo en lugar de liberarlo para atender otras peticiones, y le estás dando al cliente cero visibilidad del progreso — o recibe la respuesta final o un timeout, sin nada intermedio.
La solución es dejar de tratar "enviar el trabajo" y "obtener el resultado" como un solo intercambio HTTP, y en su lugar aceptar el trabajo de inmediato, para que el cliente vuelva después por la respuesta.
#El patrón: 202 Accepted más un recurso de estado
El cliente inicia el trabajo con un POST normal, pero el servidor responde en el momento en que el trabajo queda en cola, no cuando está terminado:
POST /videos/9f2c/transcode HTTP/1.1Host: api.example.comContent-Type: application/json{ "profile": "1080p", "format": "mp4" }
HTTP/1.1 202 AcceptedLocation: /videos/9f2c/transcode/jobs/b7e1Retry-After: 5Content-Type: application/json{"jobId": "b7e1","status": "queued","statusUrl": "/videos/9f2c/transcode/jobs/b7e1"}
202 Accepted es el estado correcto aquí, y vale la pena distinguirlo de sus vecinos: 200 OK dice "aquí tienes tu resultado terminado ahora mismo", 201 Created dice "el recurso ahora existe exactamente como se devolvió", y 202 Accepted dice "recibí la petición, el procesamiento no necesariamente ha terminado, y no prometo que vaya a tener éxito". La cabecera Location apunta a un recurso que representa el propio trabajo, y la cabecera Retry-After (segundos, o una fecha HTTP) le dice a un cliente bien portado cuánto esperar antes de volver a consultar en lugar de martillar el endpoint.
El cliente entonces hace polling sobre ese recurso de trabajo:
curl -s https://api.example.com/videos/9f2c/transcode/jobs/b7e1
{"jobId": "b7e1","status": "processing","progress": 42,"statusUrl": "/videos/9f2c/transcode/jobs/b7e1"}
...hasta que eventualmente llega a un estado terminal:
{"jobId": "b7e1","status": "completed","result": {"downloadUrl": "https://cdn.example.com/videos/9f2c-1080p.mp4","durationMs": 812345}}
Si el trabajo falla, ese mismo recurso debería reportarlo — y este es un lugar natural para reutilizar la forma de RFC 7807 Problem Details, así un trabajo asíncrono fallido reporta errores de la misma manera que lo haría una petición síncrona fallida:
{"jobId": "b7e1","status": "failed","error": {"type": "https://api.example.com/errors/unsupported-codec","title": "Unsupported Codec","detail": "El archivo fuente usa un códec de video que no se puede transcodificar al perfil solicitado."}}
#Polling vs. webhooks vs. quedarse síncrono
| Enfoque | Complejidad del cliente | Latencia de la notificación | Requisitos del servidor |
|---|---|---|---|
| Síncrono | El más simple — una petición, una respuesta | Ninguna, pero se rompe en trabajos largos (timeouts) | Ocupa un worker durante toda la duración |
| Polling | Bucle manejado por el cliente con backoff | Acotada por el intervalo de polling | No necesita conectividad entrante de tu lado |
| Webhooks | Debe exponer un endpoint HTTPS alcanzable | Push casi instantáneo | Necesita retry/backoff y verificación de firma en ambos lados |
El polling es simple y funciona en todas partes — el cliente no necesita aceptar conexiones entrantes, algo que importa mucho para cualquier cosa que corra detrás de NAT, en una función serverless, o en la laptop de alguien. Su costo son las peticiones desperdiciadas cuando nada ha cambiado todavía, y una demora de notificación acotada por qué tan agresivo hace polling el cliente (para lo cual existe exactamente Retry-After — le dice al cliente con qué agresividad se le permite hacer polling en lugar de que lo adivine).
Los webhooks invierten la dirección: en lugar de que el cliente pregunte, tu servidor llama a la URL del cliente en el instante en que el trabajo termina. Eso es menor latencia y menos tráfico desperdiciado, pero empuja complejidad real hacia ambos lados — tu servidor necesita lógica de reintento con backoff exponencial para cuando el endpoint del cliente esté caído momentáneamente, ambos lados necesitan manejar que la misma entrega llegue más de una vez (así que el manejador del webhook del cliente debe ser idempotente), y el cliente necesita alguna forma de verificar que el callback realmente vino de ti y no de un atacante que adivinó la URL:
const crypto = require('crypto');function verifyWebhookSignature(rawBody, signatureHeader, secret) {const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');const provided = signatureHeader.replace('sha256=', '');return (provided.length === expected.length &&crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided)));}
En la práctica, los sistemas que la gente pone como ejemplo de buen diseño (Stripe, la mayoría de las APIs de procesamiento de video/IA en la nube) ofrecen ambos: un webhook si el cliente lo configuró, con el polling siempre disponible como respaldo y como lo que se usa en desarrollo local cuando no hay ninguna URL pública donde recibir un callback.