Servicios externos resilientes en Express.js | Nicolás Garzón
Una llamada externa puede fallar, tardar o responder de forma ambigua. La resiliencia consiste en limitar el daño y conservar una interpretación correcta del resultado.
Express suele depender de pagos, correo, inventario remoto o APIs internas. La red introduce fallos parciales:
Texto
Copiar request local
↓
servicio externo
↓
response, timeout o resultado ambiguoToda llamada necesita límite:
TypeScript
Copiar const response = await fetch ( url, {
signal: AbortSignal. timeout ( 2_000 ) ,
} ) ; Un timeout deja de esperar; no garantiza que el proveedor detenga el trabajo. La operación remota puede haber ocurrido.
El timeout externo debe caber dentro del deadline total:
Texto
Copiar proxy 10s
→ app 8s
→ dependency 2s
→ margen para mapear y responderNo permitas que cada dependencia consuma el timeout completo de la request.
Reintenta únicamente cuando:
El error es temporal.
La operación es idempotente o usa key.
Existe tiempo restante.
El proveedor permite retry.
Usa exponential backoff y jitter. Los retries inmediatos sincronizados pueden empeorar una caída.
Un timeout de pago no significa pago rechazado. Registra estado unknown o pending, consulta al proveedor y reconcilia usando una referencia idempotente.
Un breaker evita insistir contra una dependencia claramente caída:
Texto
Copiar closed → llamadas normales
open → fallo rápido
half-open → pruebas limitadasNo sustituye timeouts. Debe medirse y configurarse por dependencia/operación.
Limita concurrencia por integración para que un proveedor lento no consuma todos los sockets o conexiones. Colas, semáforos y pools separados crean aislamiento.
Un fallback solo es válido si conserva semántica:
Mostrar cache stale para catálogo puede servir.
Inventar confirmación de pago no.
Enviar email más tarde puede ir a cola.
La respuesta externa también es no confiable. Verifica status, Content-Type, schema y límites. No asumas que un 200 contiene el JSON esperado.
Timeout de upstream → 504 cuando actúas como gateway.
Servicio temporalmente no disponible → 503.
Respuesta inválida → 502.
El contrato puede variar, pero no expongas URLs internas, tokens o bodies sensibles.
Dependency name y operation.
Duración.
Status category.
Timeout/retry count.
Circuit state.
Correlation ID.
Resultado final.
Evita alta cardinalidad con URLs completas o IDs arbitrarios en métricas.
La orden ya fue confirmada en DB.
Outbox crea job create_delivery.
Worker llama proveedor con idempotency key orderId.
Timeout deja job pendiente, no crea otro envío ciegamente.
Retry consulta por reference antes de repetir.
Si confirma, guarda external ID.
Después de límite, dead-letter y alerta.
Timeout antes y después de aceptación remota.
429 con Retry-After.
500 temporal.
JSON inválido.
Retry idempotente.
Circuit open y recovery.
Cancelación del cliente.
Llamadas sin timeout.
Retry de POST no protegido.
Fallback que oculta pérdida de consistencia.
Circuit breaker global para todo.
Mantener transacción DB durante red.
Tratar timeout como rechazo definitivo.
La red produce resultados ambiguos.
Timeouts, retries y breakers resuelven problemas distintos.
Toda mutación externa necesita idempotencia o reconciliación.
Aísla recursos por dependencia.
Fallbacks deben preservar significado.
¿Por qué timeout no significa cancelación?
¿Cuándo es seguro reintentar?
¿Qué diferencia existe entre breaker y timeout?
¿Cómo manejarías un pago ambiguo?
Ver respuestas
Porque el proveedor puede continuar aunque dejemos de esperar.
Cuando el fallo es temporal y la operación es idempotente.
El timeout limita una llamada; el breaker evita nuevas llamadas durante una caída.
Guardando estado pendiente y reconciliando por referencia idempotente.
Background jobs y colas mueve trabajo durable fuera del lifecycle HTTP.