The problem. A circuit breaker learns from traffic, and serverless keeps throwing that knowledge away:
Cold starts reset everything. A new instance starts with a closed circuit, a full token bucket and an empty failure history.
State is per instance. Lambda runs one request at a time per instance. With 100 warm instances, each one sees 1% of
the traffic, so each one needs its own run of failures before its circuit opens: 100× the failed calls a single server would make.
Cold starts look like failures. The first calls on a new instance pay for TLS handshakes, connection setup and a cold
JIT. A breaker with consecutiveFailures: 3 can open on a perfectly healthy dependency.
Invocations have a hard deadline. Lambda kills the invocation at its timeout. Work still running then is lost, with no error handling.
The tools: snapshot() / initialState to carry state between instances, sharedCircuitState to share it live,
warmUpDuration for cold starts, and withTimeout around the handler for the deadline.
Pipelines are stateful. Build them at module scope (or lazily on first use) so warm invocations reuse the same breaker and
limiters. A pipeline built inside the handler starts from scratch on every request: its breaker can never open.
CircuitBreakerStateProvider.snapshot() returns the breaker’s state as plain JSON: state, when an open circuit lets a probe
through, the consecutive-failure count and the rolling window. Pass it back as initialState and a new instance resumes
where the old one stopped. A circuit that was half-open resumes as open and due for a probe, because the probe that was in
flight is gone.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
ResiliencePipelineBuilder, type
(alias) interfaceCircuitSnapshot
importCircuitSnapshot
JSON-serializable breaker state, for persisting across restarts and serverless invocations.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
ResiliencePipelineBuilder<
interfaceResponse
The Response interface of the Fetch API represents the response to a request.
CircuitBreakerOptions<Response>.consecutiveFailures?: number |undefined
Break after this many failures in a row (a success resets the count). Simple and good for low traffic.
When set without failureRatio, the ratio check is off and only this rule applies.
consecutiveFailures: 5, // per-isolate traffic is low: a ratio over 100 calls would never trip
Current state, rolling window included, as plain JSON; pass it back as initialState to resume.
snapshot())));
}
}
},
};
For Lambda, sharedCircuitState does the bookkeeping: it pulls the shared state when the instance starts, pushes every
local state change, and pulls again every syncInterval while the instance runs. One instance seeing the payment API fail
opens the circuit for all of them.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
Shares the breaker's state with other instances (servers, processes) through a store, e.g. RedisCircuitStateStore:
one instance seeing the dependency fail opens the circuit for all of them.
addCircuitBreaker({ extensions: [sharedCircuitState({ store, key: 'payments' })] })
sharedCircuitState } from'resilience-pipeline';
import {
classRedisCircuitStateStore
CircuitStateStore over Redis: a write only lands if its updatedAt is newer than the stored one. Keys don't expire.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
ResiliencePipelineBuilder<
interfaceResponse
The Response interface of the Fetch API represents the response to a request.
CircuitBreakerOptions<Response>.consecutiveFailures?: number |undefined
Break after this many failures in a row (a success resets the count). Simple and good for low traffic.
When set without failureRatio, the ratio check is off and only this rule applies.
Background behaviour plugged into the breaker, e.g. circuitHealthCheck({ check, interval }) or
sharedCircuitState({ store, key }) from this package. Kept out of the core breaker so you only bundle what you use.
Shares the breaker's state with other instances (servers, processes) through a store, e.g. RedisCircuitStateStore:
one instance seeing the dependency fail opens the circuit for all of them.
addCircuitBreaker({ extensions: [sharedCircuitState({ store, key: 'payments' })] })
Aborted when the caller cancels, or when a strategy (timeout, hedging) gives up on this execution.
signal));
The sync timer is unref’d and Lambda freezes it between invocations, so it costs nothing while idle. See
multi-instance for how conflicting states are resolved.
What to save, and when:
Save on state changes, not per request. Writes on onOpened / onClosed are rare. Cloudflare KV in particular
limits writes per key to about one per second, and reads may lag writes by up to a minute in other locations; for breaker
state that lag is acceptable, for anything stricter use a Durable Object or Redis.
The rolling window is in the snapshot (window), so a ratio-based breaker keeps its failure history. With
consecutiveFailures the count is saved too, but only as of the last write; failures between two writes are lost, which
only delays an opening.
Only restore snapshots you wrote.initialState trusts its top-level fields. Malformed window data is ignored, but
a state that isn’t one of 'closed' | 'open' | 'half-open' | 'isolated' is not rejected. Validate if the store is shared with other code.
warmUpDuration keeps the breaker from opening for a while after it is created. Failures are still recorded, so if the
dependency really is down, the window is already full when warm-up ends and the circuit opens on the next failure.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
CircuitBreakerOptions<any>.consecutiveFailures?: number |undefined
Break after this many failures in a row (a success resets the count). Simple and good for low traffic.
When set without failureRatio, the ratio check is off and only this rule applies.
Warm-up only prevents new openings. A circuit restored as open from initialState stays open until its break ends.
CircuitBreakerStateProvider.isWarmingUp tells you whether an instance is still in warm-up, for logs or health checks.
A per-instance limiter that protects a quota (a cron function that may send 100 calls per minute) gets a fresh full bucket on
every cold start. Save the limiter’s state at the end of the run and restore it on the next start; time that passed in
between counts (tokens refill, windows expire, blocks run out).
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
Loads a snapshot(); time passed since it was taken counts (windows expire, tokens refill, blocks run out).
restore(
varJSON:JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
JSON.
JSON.parse(text: string, reviver?: (this:any, key:string, value:any) => any): any
Converts a JavaScript Object Notation (JSON) string into an object.
@param ― text A valid JSON string.
@param ― reviver A function that transforms the results. This function is called for each member of the object.
If a member contains nested objects, the nested objects are transformed before the parent object is.
@throws ― {SyntaxError} If text is not valid JSON.
parse(
constsaved:string
saved));
} catch {
// Throws TypeError on a malformed or incompatible snapshot (e.g. after changing segmentsPerWindow): start fresh.
The algorithm's state and the block, JSON-serializable, for restore after a restart.
snapshot()));
}
Every in-memory limiter supports this: token bucket, fixed and sliding window, and BurstyRateLimiter built from them.
A partitioned limiter’s snapshot(limit?) saves the limit most recently used partitions (default: all), so the saved
state stays small with many keys. restore() recreates them.
If several instances run at the same time, saving and restoring doesn’t make them share a limit: each restores the same
state and then spends it independently. Use a Redis-backed limiter for that (multi-instance).
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
With unrefTimers: true, pending retry delays and timeouts don’t keep a Node or Bun process alive (or Deno ≥ 2.8; older Deno timers have no unref(), so it has no effect there). A CLI, a cron
script or a queue worker exits when its real work is done, instead of waiting out a 30 s retry delay that nobody needs.
The option is ignored when you pass your own timeProvider, and makes no difference on Workers and in browsers.
On Lambda, the invocation is killed when its configured timeout passes. Wrap the handler body in withTimeout with the
remaining time minus a margin. Every pipeline executed underneath inherits that deadline through AsyncLocalStorage, so
calls that are still waiting are aborted in time to return a proper error, release locks or requeue the message.
The console module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
A global console instance configured to write to process.stdout and
process.stderr. The global console can be used without importing the node:console module.
Warning: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O for
more information.
Example using the global console:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(newError('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
constname='Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console class:
constout=getStreamSomehow();
consterr=getStreamSomehow();
constmyConsole=new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(newError('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
Prints to stderr with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3)
(the arguments are all passed to util.format()).
constcode=5;
console.error('error #%d', code);
// Prints: error #5, to stderr
console.error('error', code);
// Prints: error 5, to stderr
If formatting elements (e.g. %d) are not found in the first string then
util.inspect() is called on each argument and the
resulting string values are concatenated. See util.format()
for more information.
Appends new elements to the end of an array, and returns the new length of the array.
@param ― items New elements to add to the array.
push({
itemIdentifier: string
itemIdentifier:
constrecord: {
messageId:string;
body:string;
}
record.
messageId: string
messageId }); // SQS redelivers only these
}
}
return {
batchItemFailures: {
itemIdentifier: string;
}[]
batchItemFailures:
constfailures: {
itemIdentifier:string;
}[]
failures };
};
On Workers there’s no fixed wall-clock deadline per request, but there is a client that can go away. If your framework gives
you a request signal that aborts on disconnect, pass it as execute(fn, { signal }); Hono’s middleware does that for you.