Resilience Basics
8 examples to get you started with Resilience for Node.js backends - 6 basic and 2 intermediate.
Prerequisites
- Node.js 24.18.0 with native
fetchandAbortSignal.timeout. - HTTP service behind Kubernetes, ECS, or similar for health/readiness probes.
Basic Examples
1. Timeout Outbound fetch
Hung downstream calls exhaust your connection pool.
const response = await fetch("https://api.partner.com/data", {
signal: AbortSignal.timeout(5_000),
});AbortSignal.timeoutavailable in Node 18+ - no manualsetTimeout.- Map
TimeoutErrorto 504 or retry policy upstream. - Shorter timeouts on user-facing paths than batch jobs.
Related: Timeouts Everywhere - full timeout matrix
2. Fail Fast on Boot Config
Do not serve traffic with invalid environment.
import { z } from "zod";
const Env = z.object({
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().default(3000),
});
export const env = Env.parse(process.env); // throws before listen()- Process exits non-zero - orchestrator restarts with fixed config.
- Readiness stays false until DB migration completes if you gate on that.
- Better one failed deploy than silent runtime errors on every request.
Related: Configuration Basics - Zod env parse
3. Liveness vs Readiness
Tell Kubernetes whether to restart vs stop sending traffic.
app.get("/health", (_req, res) => res.json({ ok: true }));
app.get("/ready", async (_req, res) => {
try {
await db.query("SELECT 1");
res.json({ ready: true });
} catch {
res.status(503).json({ ready: false });
}
});- Liveness: process not deadlocked - cheap check.
- Readiness: dependencies available - fails when DB down so kube removes from LB.
- Do not put slow checks on liveness - restart loops.
Related: Observability Basics - probe patterns
4. Graceful Shutdown on SIGTERM
Drain in-flight HTTP before exit on deploy.
const server = app.listen(3000);
process.on("SIGTERM", () => {
server.close(() => {
logger.info({ event: "shutdown_complete" });
process.exit(0);
});
setTimeout(() => process.exit(1), 30_000).unref();
});server.closestops accepting new connections; finishes active ones.- Force exit after 30s if drain stalls - match k8s
terminationGracePeriodSeconds. - Close DB pools and OTel SDK in same shutdown handler.
Related: Graceful Degradation - feature-off modes
5. Retry Only When Safe
POST without idempotency keys can double-charge.
async function safeGetWithRetry(url: string, attempts = 3): Promise<Response> {
for (let i = 0; i < attempts; i++) {
try {
const res = await fetch(url, { signal: AbortSignal.timeout(3_000) });
if (res.status < 500) return res;
} catch (err) {
if (i === attempts - 1) throw err;
}
await delay(100 * 2 ** i);
}
throw new Error("unreachable");
}- Retry GET and idempotent PUT - safe by HTTP semantics (with caveats).
- Retry 5xx, not 4xx (client errors will not heal).
- Exponential backoff reduces thundering herd.
Related: Retries with Backoff - jitter and idempotency
6. Return Explicit Errors, Not Hangs
Users and load balancers need a response within SLA.
app.use((req, res, next) => {
const timer = setTimeout(() => {
if (!res.headersSent) {
res.status(504).json({ error: "Gateway Timeout" });
}
}, 30_000);
res.on("finish", () => clearTimeout(timer));
next();
});- Server-level timeout is last resort - prefer per-outbound timeouts.
- Log 504 with
requestIdfor correlation. - Align with LB idle timeout (often 60s).
Related: Timeouts Everywhere - server timeouts
Intermediate Examples
7. Circuit Breaker Sketch
Stop calling a failing dependency to let it recover.
let failures = 0;
const THRESHOLD = 5;
let openUntil = 0;
async function callBilling() {
if (Date.now() < openUntil) throw new Error("Circuit open");
try {
const result = await billingClient.charge();
failures = 0;
return result;
} catch (err) {
failures++;
if (failures >= THRESHOLD) openUntil = Date.now() + 30_000;
throw err;
}
}- Open circuit fast-fails local requests - saves threads and downstream load.
- Half-open retry after cooldown - production use
opossumlibrary. - Pair with graceful degradation for user experience.
Related: Circuit Breakers - opossum patterns
8. Bulkhead: Limit Concurrent Outbound Calls
One slow vendor cannot consume all sockets.
import { Semaphore } from "async-mutex"; // or custom pool
const vendorSem = new Semaphore(10);
async function callVendor() {
const [, release] = await vendorSem.acquire();
try {
return await fetch(VENDOR_URL, { signal: AbortSignal.timeout(2_000) });
} finally {
release();
}
}- Cap concurrent calls to fragile integration - bulkhead pattern.
- Remaining requests fail fast locally instead of queuing forever.
- Tune limit from vendor SLA and your pool size.
Related: Circuit Breakers - bulkhead with opossum
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.