What a circuit breaker protects
A circuit breaker sits between your service and a dependency such as a payment provider, search cluster, feature-flag service, or internal API. When recent calls show that dependency is unhealthy, the breaker stops sending it more work for a short, controlled period. Callers receive a fast, intentional result instead of waiting behind a growing queue of doomed requests.
Closed
Calls flow normally while the breaker measures failures and latency.
Open
Calls fail fast or use a fallback, protecting the dependency and your own capacity.
Half-open
A small number of probe requests test whether the dependency has recovered.
The goal is not to hide every outage. It is to contain one dependency's failure so users can still receive the parts of the product that remain safe and useful. Pair this with the request IDs, traces, and alerts described in our Next.js observability guide so an open breaker is an explainable operational event, not a surprise.
Place breakers around remote, finite, or failure-prone work
A breaker is most valuable at a boundary where a failure can consume scarce resources or spread across requests. External HTTP calls are common candidates, but database replicas, queues, cache clusters, and internal services can qualify too. Do not wrap every function call: local validation and deterministic business rules should fail directly, where their error remains clear.
- Use one breaker per meaningful dependency and operation, such as
payments.capturerather than one global breaker for every vendor call. - Give each breaker a stable name that appears in logs, metrics, dashboards, and incident notes.
- Keep tenant, user, and raw URL values out of breaker keys; high-cardinality keys hide fleet-wide health and complicate recovery.
- Share state deliberately across instances when the dependency is globally unhealthy; per-process state may be enough for local overload protection.
Choose the boundary before the first network call, not after a helper has started retries. This keeps timeouts, retry policy, and fallback behavior in one place. For database capacity specifically, combine the pattern with the limits in our Node.js connection-pooling guide; a breaker cannot compensate for an unbounded acquisition queue.
Use timeouts and retry budgets before counting failures
A breaker cannot make an unbounded request safe. Set a timeout shorter than the caller's remaining deadline, abort the underlying request when that timeout fires, and classify the outcome. Timeouts, connection errors, and selected 5xx responses may count as dependency failures; a caller's invalid request or a deliberate 4xx response generally should not.
For non-idempotent operations such as charging a card, use an idempotency key or a status lookup before retrying. When the response is uncertain, report a pending outcome and reconcile it asynchronously rather than issuing a duplicate action. The same idempotency discipline is useful for workers; see our Node.js background jobs and queues guide.
Design fallbacks that are honest and safe
A fallback should preserve a truthful, useful experience—not invent a successful result. A product catalog may show a short-lived cached response. A recommendation panel may be omitted. A payment capture should usually return a clear retryable or pending status, because pretending it succeeded can create financial and support problems.
Cached data
Use bounded, clearly stale-tolerant data for non-critical reads.
Reduced experience
Defer an optional feature while the core journey keeps working.
Async recovery
Accept safe work, record it durably, and complete it when the dependency recovers.
Make fallback ownership explicit. Document what data can be stale, how long it can be shown, and who reconciles pending work. If a feature flag controls the fallback, use targeted rollout and cleanup rules from our Next.js feature-flags guide instead of leaving emergency behavior permanently ambiguous.
Implement the state machine as a small, observable boundary
The implementation below illustrates the behavior rather than prescribing a specific library. In a multi-instance service, put the shared counter and open-until timestamp in a suitable coordination store if all instances need the same view. Keep the dependency call itself injectable so its error handling can be tested independently.
const state = { failures: 0, openUntil: 0, probing: false };
async function withCircuitBreaker(call, fallback) {
const now = Date.now();
if (state.openUntil > now && !state.probing) return fallback("circuit_open");
state.probing = state.openUntil > 0;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 1_500);
try {
const result = await call({ signal: controller.signal });
state.failures = 0;
state.openUntil = 0;
return result;
} catch (error) {
state.failures += 1;
if (state.failures >= 5) state.openUntil = Date.now() + 30_000;
return fallback(error.name === "AbortError" ? "timeout" : "dependency_failed");
} finally {
state.probing = false;
clearTimeout(timeout);
}
}Production code needs a few refinements: limit half-open probes with a semaphore, use a rolling time window instead of an unbounded failure count, and reset state only after a successful probe. Log a state transition once rather than logging every short-circuited call at error level. The precise numbers should come from a dependency's normal latency, error rate, capacity, and user impact—not copied from an example.
Measure transitions, test failure modes, and roll out gradually
Record the dependency name, breaker state, transition reason, timeout, fallback type, and release version. Track open duration, rejected-call rate, probe success rate, dependency latency, and the user-facing outcome of the fallback. These signals distinguish a healthy breaker protecting customers from a threshold that is too aggressive.
Test a delayed dependency, a connection refusal, a partial outage, and recovery. Verify that aborting a request really releases sockets and pool slots, that only a bounded number of probes run while half-open, and that a fallback cannot leak another user's data. Then introduce the breaker in shadow mode or with metrics-only logging before turning on fail-fast behavior. A safe rollback should be as deliberate as a safe deploy; our Node.js graceful-shutdown guide covers the handoff side of that release process.
Node.js circuit breaker checklist
✓ Each breaker protects one named remote dependency or operation
✓ Calls have a bounded timeout and abort underlying work
✓ Failure classification excludes expected client errors
✓ Retries are idempotent, jittered, and inside a retry budget
✓ Open breakers return a truthful, documented fallback
✓ Half-open probes are limited and recovery is measured
✓ Metrics record state transitions and user-facing outcomes
✓ Failure and recovery paths are tested before rollout
Make dependency failures survivable
Endurance Softwares helps teams build dependable Node.js and Next.js systems with clear service boundaries, production observability, secure release practices, and resilient failure handling.