Retry & Outbound Resilience
Outbound calls from Node fail transiently. Combine timeouts, retries with backoff, and circuit breakers so one slow vendor does not stall your API.
Search across all documentation pages
Outbound calls from Node fail transiently. Combine timeouts, retries with backoff, and circuit breakers so one slow vendor does not stall your API.
Quick-reference recipe card - copy-paste ready.
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 });
}],
},
});When to reach for this:
// 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 with manual idempotent retry
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; // network
const code = err.response?.statusCode ?? 0;
return code >= 500 || code === 429;
}What this demonstrates:
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 package mirrors got policy| Status | Retry? |
|---|---|
| 400 Bad Request | No - fix payload |
| 401/403 | No - fix credentials |
| 404 | No - wrong resource |
| 409 Conflict | No - unless idempotent upsert |
| 429 | Yes with backoff respecting Retry-After |
| 503 | Yes limited attempts |
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.| Alternative | Use When | Don't Use When |
|---|---|---|
| Queue retry only | User not waiting on sync path | User needs immediate partner response |
| No retry, fail fast | Optional enrichment | Payment authorization |
| Temporal activity retry | Long workflow orchestration | Simple fetch |
| Hedged requests | Extreme tail latency | Idempotent read only, doubles load |
Both maintained. got ESM-native; axios ubiquitous. Pick one per repo.
Max 1-2 quick retries inside user request. Heavy retry belongs in worker.
Stripe retries idempotent requests automatically - do not double-wrap blindly.
Counts as network error - retry with backoff; alert if persistent.
Each attempt needs timeout; total wall clock also capped.
Safe to retry if resource key stable. POST create is not without key.
MSW/nock return 503 twice then 200; assert call count and final success.
Native fetch has no retry - wrap with got/axios or custom helper.
RxJS retry operator - same idempotency rules apply.
Avoid implicit global retry - per-integration client instances clearer.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 16, 2026