Reintentos y Resiliencia Saliente
Las llamadas salientes desde Node fallan transitoriamente. Combina tiempos de espera, reintentos con retroceso y disyuntores para que un proveedor lento no detenga tu API.
Busca en todas las páginas de la documentación
Las llamadas salientes desde Node fallan transitoriamente. Combina tiempos de espera, reintentos con retroceso y disyuntores para que un proveedor lento no detenga tu API.
Tarjeta de receta de referencia rápida: lista para copiar y pegar.
import got from "got";
const client = got.extend({
timeout: { request: 8_000 },
retry: {
limit: 3,
methods: ["GET", "PUT", "HEAD", "DELETE", "OPTIONS", "TRACE"],
statusCodes: [408, 413, 429, 500, 502, 503, 504],
calculateDelay: ({ attemptCount }) => Math.min(1000 * 2 ** attemptCount, 10_000) + Math.random() * 200,
},
hooks: {
beforeRetry: [(err, retryCount) => {
console.warn({ msg: "got_retry", retryCount, err: err.message });
}],
},
});Cuándo usarlo:
// src/http/partner-client.ts
import got, { type Got } from "got";
function buildClient(): Got {
return got.extend({
prefixUrl: process.env.PARTNER_API_URL,
headers: { Authorization: `Bearer ${process.env.PARTNER_TOKEN}` },
timeout: { request: 8_000 },
retry: {
limit: 3,
methods: ["GET"],
statusCodes: [429, 500, 502, 503, 504],
calculateDelay: ({ attemptCount }) => 500 * 2 ** attemptCount + Math.random() * 100,
},
});
}
export const partnerClient = buildClient();
// POST con reintento idempotente manual
export async function createPartnerJob(idempotencyKey: string, body: unknown) {
const maxAttempts = 3;
let attempt = 0;
while (attempt < maxAttempts) {
try {
return await partnerClient.post("jobs", {
json: body,
headers: { "Idempotency-Key": idempotencyKey },
retry: { limit: 0 },
}).json();
} catch (err) {
attempt++;
if (!isRetryable(err) || attempt >= maxAttempts) throw err;
await sleep(500 * 2 ** attempt);
}
}
}
function isRetryable(err: unknown): boolean {
if (!got.isHTTPError(err)) return true; // red
const code = err.response?.statusCode ?? 0;
return code >= 500 || code === 429;
}Lo que esto demuestra:
import axios from "axios";
import axiosRetry from "axios-retry";
const api = axios.create({ baseURL: process.env.PARTNER_API_URL, timeout: 8000 });
axiosRetry(api, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (err) => axiosRetry.isNetworkOrIdempotentRequestError(err) || err.response?.status === 429,
});axios-retry refleja la política de got| Estado | ¿Reintentar? |
|---|---|
| 400 Bad Request | No - corrige la carga útil |
| 401/403 | No - corrige las credenciales |
| 404 | No - recurso incorrecto |
| 409 Conflict | No - a menos que sea una inserción/actualización idempotente |
| 429 | Sí con retroceso respetando Retry-After |
| 503 | Sí, intentos limitados |
import CircuitBreaker from "opossum";
const breaker = new CircuitBreaker(callPartner, {
timeout: 10_000,
errorThresholdPercentage: 50,
resetTimeout: 30_000,
});
breaker.fallback(() => ({ status: "degraded" }));const deadline = Date.now() + 15_000;
while (attempts--) {
const remaining = deadline - Date.now();
if (remaining <= 0) throw new Error("deadline_exceeded");
await client.get("x", { timeout: { request: remaining } });
}Retry-After.| Alternativa | Usar Cuándo | No Usar Cuándo |
|---|---|---|
| Solo reintento de cola | El usuario no está esperando en la ruta síncrona | El usuario necesita una respuesta inmediata del socio |
| Sin reintento, falla rápido | Enriquecimiento opcional | Autorización de pago |
| Reintento de actividad temporal | Orquestación de flujo de trabajo largo | Recuperación simple |
| Solicitudes protegidas | Latencia de cola extrema | Solo lectura idempotente, duplica la carga |
Ambos se mantienen. got es nativo de ESM; axios es ubicuo. Elige uno por repositorio.
Máximo 1-2 reintentos rápidos dentro de la solicitud del usuario. Los reintentos pesados pertenecen a un worker.
Stripe reintenta las solicitudes idempotentes automáticamente; no envuelvas ciegamente.
Cuenta como error de red: reintenta con retroceso; alerta si es persistente.
Cada intento necesita un tiempo de espera; el tiempo total de ejecución también está limitado.
Seguro de reintentar si la clave de recurso es estable. La creación POST no lo es sin clave.
MSW/nock devuelve 503 dos veces y luego 200; afirma el recuento de llamadas y el éxito final.
Fetch nativo no tiene reintentos; envuélvelo con got/axios o un helper personalizado.
Operador retry de RxJS; se aplican las mismas reglas de idempotencia.
Evita el reintento global implícito; las instancias de cliente por integración son más claras.
Versiones de la pila: Esta página fue escrita para Node.js 24.18.0 (LTS activa), npm 10+, TypeScript 5.6+, Express 5, Fastify 5 y NestJS 11.
Revisado por Chris St. John·Última actualización: 16 jul 2026