Open-source project
nodeshift/opossum avatar
nodeshift/opossum

opossum: a circuit breaker for Node.js promises

Node.js circuit breaker - fails fast ⚡️

1,690 stars114 forksJavaScriptApache-2.0

At a glance

What is it?
opossum wraps an asynchronous function in a breaker that watches how it is failing, stops calling it once a threshold is crossed, and lets you supply a fallback, cancel in-flight work with an AbortController and serialise the breaker state so a serverless function can resume where it left off. It suits anyone whose service calls something that sometimes fails, and it needs Node 22 or later.
Who is it for?
Adopt opossum if your service calls a dependency that fails in bursts, because the breaker stops sending traffic to something that is down and the fallback gives callers a defined answer instead of a timeout. If the failure you need to handle is a slow call rather than an error, set the timeout above the call's normal duration, since a call that exceeds it counts as a failure.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 63 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the pattern is for

Opossum is a Node.js circuit breaker that executes asynchronous functions and monitors their execution status. When things start failing, opossum plays dead and fails fast, and if you want, you can provide a fallback function to be executed when in the failure state.

That is the whole idea, and it comes from a pattern with a long history. The README points at Fowler's blog post on the circuit breaker as a place to start reading, and the topics list includes the hystrix and hystrix-dashboard names.

The problem the pattern solves is the one where a dependency starts failing and your service keeps calling it anyway. Each of those calls takes a long time to fail, so your own request threads or connections pile up, and the dependency is now failing partly because of the load you are putting on it. A circuit breaker stops the calls for a while so the dependency can recover and so you stop making it worse.

The implementation detail that matters for a JavaScript user is that the function is asynchronous and the breaker is promise shaped. You do not call the function, you fire the breaker, and the breaker returns a promise.

javascript
const CircuitBreaker = require('opossum');

function asyncFunctionThatCouldFail(x, y) {
  return new Promise((resolve, reject) => {
    // Do something, maybe on the network or a disk
  });
}

The package is Apache-2.0 licensed and authored by Red Hat, it requires Node.js 22 or later, and the documentation lives on the project site. The type definitions are not in the repository but in the DefinitelyTyped project, which is worth knowing if you are on TypeScript, since the types and the implementation are versioned separately.

The three options that decide the behaviour

The example configuration is the part of the documentation to read carefully, because the numbers are the policy.

javascript
const options = {
  timeout: 3000, // If our function takes longer than 3 seconds, trigger a failure
  errorThresholdPercentage: 50, // When 50% of requests fail, trip the circuit
  resetTimeout: 30000 // After 30 seconds, try again.
};
const breaker = new CircuitBreaker(asyncFunctionThatCouldFail, options);

breaker.fire(x, y)
  .then(console.log)
  .catch(console.error);

The timeout is not a client side convenience, it is a failure. A call that takes longer than the timeout counts as a failure, which means a dependency that is slow rather than broken will trip the breaker just as effectively as one that returns errors. Whether that is what you want depends on the dependency, and for a call that is legitimately slow the timeout should be set above its normal duration.

The error threshold percentage is the proportion of failures that trips the circuit. A percentage rather than a count, so the breaker works the same whether your service handles ten requests a second or ten thousand, and a percentage rather than a consecutive-failure count, so an intermittent error rate does not open the circuit on a bad moment.

The reset timeout is how long the circuit stays open before the breaker tries the dependency again.

Together they are the classic tuning triangle. A low threshold and a short reset timeout produce a breaker that opens and closes rapidly, which is right for a flaky dependency. A high threshold and a long reset timeout produce one that stays out of the way, which is right for a dependency that has a real outage and will not come back in thirty seconds.

AbortController support, and renewing the signal

Timeout handling in JavaScript has a problem that is specific to the language. When a promise times out, the underlying operation keeps running, because there is no way to cancel a promise. The connection stays open, the work keeps going, and the resources keep being held until the far side gives up.

Opossum addresses this with the AbortController. You provide one in the options, the wrapped function receives the signal as its first argument, and when the breaker times out the request is aborted rather than abandoned.

javascript
const abortController = new AbortController();
const options = {
  abortController,
  timeout: 3000, // If our function takes longer than 3 seconds, trigger a failure
};
const breaker = new CircuitBreaker(asyncFunctionThatCouldFail, options);

breaker.fire(abortController.signal)
  .then(console.log)
  .catch(console.error);

The renewal problem follows from that design. A signal that has been aborted stays aborted, so reusing the same controller for the next request would abort immediately. The autoRenewAbortController option handles that by renewing the controller automatically when the breaker transitions into the halfOpen or closed states, so the signal can be reused for ongoing requests without manual intervention.

javascript
const options = {
  autoRenewAbortController: true,
  timeout: 3000,
};
const breaker = new CircuitBreaker(asyncFunctionThatCouldFail, options);

const signal = breaker.getSignal();
breaker.fire(signal)
  .then(console.log)
  .catch(console.error);

The getSignal method is the accessor for the current signal, and the pattern of asking the breaker for it rather than holding your own reference is what makes the renewal work.

Fallbacks, the fallback event, and what counts as failure

The fallback is the other half of the pattern, and opossum's rules around it are worth reading precisely.

You register a fallback function, and you listen for the fallback event to take some action when the fallback is performed.

javascript
const breaker = new CircuitBreaker(asyncFunctionThatCouldFail, options);
// if asyncFunctionThatCouldFail starts to fail, firing the breaker
// will trigger our fallback function
breaker.fallback(() => 'Sorry, out of service right now');
breaker.on('fallback', (result) => reportFallbackEvent(result));

The rule that catches people is this: when a fallback function is triggered, it is considered a failure, and the fallback function will continue to be executed until the breaker is closed. A fallback is not a way to make the breaker happy. Serving stale data or a canned response is a degraded service, and the breaker keeps counting it as a failure so that the circuit stays open until the real dependency recovers.

That is the correct behaviour, and it is the opposite of what a well meaning implementation would do.

The fallback function accepts the same parameters as the fire function, so the arguments are passed through.

javascript
const breaker = new CircuitBreaker(delay);
breaker.fire(20000, 1, 2, 3);
breaker.fallback((delay, a, b, c) => `Sorry, out of service right now. But your parameters are: ${delay}, ${a}, ${b} and ${c}`);

That example is also a reminder that the fallback is called with the same arguments, so a fallback that ignores them is usually the one you want, and a fallback that logs them needs to accept them.

The state machine: open, halfOpen and closed

The lifecycle is described in a few sentences in the documentation, and it is the classic one.

Once the circuit has opened, a timeout is set based on the resetTimeout option. When the resetTimeout expires, opossum enters the halfOpen state. In that state, the next time the circuit is fired, the action is executed again. If it succeeds, the circuit closes and emits the close event. If the action fails or times out, it immediately re-enters the open state.

So there are three states, and the halfOpen state is a single trial rather than a gradual ramp. The breaker's promise to the caller is that a call during the open state fails immediately rather than waiting for a dependency that is known to be down, and that after the reset timeout exactly one call is let through to test the water.

The events are the observability surface. A fallback event fires when the fallback runs, a close event fires when the circuit closes, and the documentation also refers to the open state and the shutdown flag, which appears in the state object, and the warmUp flag, which is how a breaker can be told to ignore the first calls while a service starts up.

For a service that wants to expose breaker state as a metric, the transition events are the thing to wire into your instrumentation, and the state object described next is the thing to report.

Serialising the state for ephemeral and serverless runtimes

There is a section of the documentation devoted to a problem that only exists because of how containers work, and it is the part of the library I would read first if I were evaluating it.

Sometimes you need to initialise the state of a circuit breaker. The primary use case is a serverless environment such as Knative or AWS Lambda, or any container based platform, where the container being deployed is ephemeral.

The toJSON method is a helper that returns the current state and status of the breaker.

code
const breakerState = breaker.toJSON();

The object it returns has a state part with the enabled flag, the name, the closed, open and halfOpen booleans, the warmUp flag and the shutdown flag, and a status part with the statistics.

A new circuit breaker instance can be created with that state by passing the object in.

code
const breaker = new CircuitBreaker({state: state});

And there is a companion for the status object, to pre-populate the cumulative statistics in the same serverless and ephemeral container situations.

The reason this matters is not obvious until you think about what happens without it. A container that is recreated on every request comes up with a fresh breaker, and a fresh breaker is closed, and a closed breaker lets the first request through to a dependency that is still broken. The request fails, the threshold is not reached, and the container is destroyed before the breaker ever opens. Every request pays the full timeout, and the breaker protects nothing.

Persisting the state across invocations is what makes the pattern work in that environment, and opossum gives you the two pieces you need for it, the serialisation and the constructor that accepts one.

The project itself, and how it is tested

The package is at version 10.0.0, with 9.0.0 from June 2025 and 8.5.0 from May 2025, and the last push to the repository was on 2026-08-21. The major version bump suggests a project that is willing to break compatibility, and the node engine requirement of 22 or later in the project table says where it is anchored.

The build and test scripts in the manifest describe a project with more than one testing environment, which is unusual and interesting for a library.

json
    "test": "nyc tape test/*.js | faucet",
    "test:headless": "node test/browser/webpack-test.js",
    "test:browser": "opener http://localhost:9007/test/browser/index.html && serve . -p 9007",
    "test:all": "npm run build && npm run test && npm run test:headless"

The unit tests run with tape and are measured for coverage with nyc, and the results are piped through faucet, which is the tool that turns a test output into something readable. There is a headless browser test that runs a webpacked test bundle, and a browser test that opens the test page on port 9007 and serves the directory, so a library that has a browser build has browser tests.

That browser build matters. The build script compiles a browser bundle with webpack using a configuration in the config directory, and the published files include the dist directory, so opossum can be used in a browser as well as in Node. The docs are generated with the documentation tool in both HTML and Markdown, and there is a script that regenerates the browser test index first, which keeps the test page in step with the test files.

The lint configuration is eslint with the standard and semistandard configurations, husky manages the commit hooks, and the package is CommonJS with the entry point at index.js.

Editorial conclusion

Adopt opossum if your service calls a dependency that fails in bursts, because the breaker stops sending traffic to something that is down and the fallback gives callers a defined answer instead of a timeout. If the failure you need to handle is a slow call rather than an error, set the timeout above the call's normal duration, since a call that exceeds it counts as a failure. Before you deploy, decide whether your runtime can hold breaker state, and if it is serverless, read the state serialisation section, since a container that is recreated with a fresh breaker starts closed and will take the failure again before it trips.

Frequently asked questions

What does opossum do?

It is a Node.js circuit breaker that executes asynchronous functions and monitors their execution status. When things start failing it plays dead and fails fast, and you can provide a fallback function to be executed when in the failure state.

Which options control the opossum circuit breaker?

The timeout in milliseconds, which counts a slow call as a failure, the errorThresholdPercentage which is the proportion of failures that trips the circuit, and the resetTimeout which is how long the circuit stays open before the dependency is tried again. The example uses 3000, 50 and 30000.

How does opossum support AbortController?

You provide an AbortController in the options and the wrapped function receives the signal, so the request is aborted when the breaker times out instead of being abandoned. The autoRenewAbortController option renews the controller automatically when the breaker transitions into the halfOpen or closed states, and breaker.getSignal() returns the current signal.

How do I keep circuit breaker state in a serverless function?

Use the toJSON method to get the current state and status of the breaker, and pass that object to a new CircuitBreaker as the state option. There is a companion mechanism for pre-populating the cumulative statistics, both aimed at ephemeral container and serverless platforms where a fresh breaker would otherwise start closed on every invocation.

Official sources

  1. License: Apache-2.0
  2. nodeshift/opossum on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/nodeshift-opossum.svg)](https://hysenlabs.com/projects/nodeshift-opossum)

Was this useful?