Circuit Breaker in Node.js and NestJS with Opossum, Fallback and Concurrency Control

How I implement circuit-breaker behavior in Node.js/NestJS using Opossum-style policies, including timeouts, singleton breaker state, fallback, metrics and bounded concurrency.

Romharshan Singh
Romharshan SinghSenior Solution Architect • AI & Cloud Mentor
1 September 20264 min read0 viewsUpdated 1 Sept 2026
Circuit Breaker in Node.js and NestJS with Opossum, Fallback and Concurrency Control

Circuit Breaker in Node.js and NestJS with Opossum, Fallback and Concurrency Control

Why Node services need fast failure

Node can handle many concurrent I/O operations, but that does not mean an unlimited number of pending requests is safe.

If a downstream supplier takes thirty seconds:

plaintext
pending Promises
open sockets
response objects
memory
upstream requests

accumulate.

A circuit breaker lets the service stop creating more slow work after the dependency crosses a health threshold.

Opossum-style breaker

typescript
import CircuitBreaker from 'opossum';

const options = {
  timeout: 1800,
  errorThresholdPercentage: 50,
  resetTimeout: 15000,
  volumeThreshold: 10,
};

const supplierBreaker =
  new CircuitBreaker(
    async (request: SupplierRequest) =>
      supplierClient.search(request),
    options,
  );

supplierBreaker.fallback(
  (request: SupplierRequest) => ({
    supplier: request.supplier,
    status: 'TEMPORARILY_UNAVAILABLE',
    results: [],
  }),
);

The breaker should be a long-lived singleton/provider.

This is wrong:

typescript
async function call() {
  const breaker = new CircuitBreaker(...);
  return breaker.fire();
}

A new breaker per request forgets all historical health information.

NestJS provider

typescript
@Injectable()
export class SupplierCircuit {
  private readonly breaker: CircuitBreaker<
    [SupplierRequest],
    SupplierResponse
  >;

  constructor(
    private readonly supplierClient: SupplierClient,
    private readonly metrics: MetricsService,
  ) {
    this.breaker = new CircuitBreaker(
      (request) =>
        this.supplierClient.search(request),
      {
        timeout: 1800,
        errorThresholdPercentage: 50,
        resetTimeout: 15000,
        volumeThreshold: 10,
      },
    );

    this.breaker.on('open', () =>
      this.metrics.increment(
        'supplier.breaker.open',
      ),
    );

    this.breaker.on('halfOpen', () =>
      this.metrics.increment(
        'supplier.breaker.half_open',
      ),
    );

    this.breaker.on('close', () =>
      this.metrics.increment(
        'supplier.breaker.close',
      ),
    );

    this.breaker.on('timeout', () =>
      this.metrics.increment(
        'supplier.timeout',
      ),
    );
  }

  execute(request: SupplierRequest) {
    return this.breaker.fire(request);
  }
}

Circuit breaker is not a bulkhead

If a dependency becomes slow before the breaker has enough samples to open, large concurrency can already accumulate.

I add a concurrency limit:

typescript
import pLimit from 'p-limit';

const supplierLimit = pLimit(20);

async function protectedSearch(
  request: SupplierRequest,
) {
  return supplierLimit(
    () => supplierBreaker.fire(request),
  );
}

Now the dependency receives at most twenty concurrent requests from this instance.

Retry with Opossum

I normally place a very small retry inside the protected action or a shared HTTP client, not outside in several layers.

typescript
async function callSupplier(
  request: SupplierRequest,
) {
  for (let attempt = 1; attempt <= 2; attempt++) {
    try {
      return await withTimeout(
        () => supplierClient.search(request),
        1200,
      );
    } catch (error) {
      if (
        attempt === 2 ||
        !isTransient(error)
      ) {
        throw error;
      }

      await sleep(
        100 * 2 ** (attempt - 1) +
        Math.floor(Math.random() * 70),
      );
    }
  }
}

The breaker sees the final action result while the retry count stays tightly bounded.

Fallback

Good search fallback:

json
{
  "status": "DEGRADED",
  "source": "SUPPLIER_A",
  "results": []
}

Good read fallback:

plaintext
serve cached reference data
mark freshness timestamp

Bad payment fallback:

plaintext
pretend capture succeeded

Metrics

I track:

  • breaker state transition;
  • fires;
  • failures;
  • timeouts;
  • rejected/not-permitted;
  • fallback count;
  • action duration;
  • in-flight concurrency;
  • queue/wait time around the bulkhead.

Event loop health

A remote I/O outage can coincide with local event-loop pressure.

I also observe:

plaintext
event loop delay
heap used
active sockets
HTTP agent socket count
GC pauses

A breaker protects dependency calls but does not fix CPU blocking inside the Node process.

Production checklist

  • Register one breaker instance per dependency/policy.
  • Use HTTP timeout below overall SLA.
  • Add bounded concurrency.
  • Keep retries small.
  • Add jitter.
  • Define explicit degraded response.
  • Export state and rejection metrics.
  • Monitor event-loop delay.
  • Load-test dependency latency, not only HTTP 500.
  • Test breaker recovery.

FAQ

Can NestJS use Resilience4j?

No, Resilience4j is Java. In Node/NestJS I use Node-native libraries or shared policy wrappers.

Should breaker state be centralized in Redis?

Usually not. Local breaker state is faster and avoids another dependency.

Does Opossum limit all concurrency?

Circuit breaking and concurrency limiting are different concerns. I add a semaphore/p-limit/bulkhead where capacity must be bounded.

- [Microservices Design Patterns I Have Used in Real Enterprise Projects](/articles/microservices-design-patterns-real-enterprise-projects) - [Circuit Breaker with Resilience4j](/articles/circuit-breaker-resilience4j-spring-boot)
  • Backpressure in Kafka and NestJS

  • Retry Pattern

    Architect's final take

    In Node systems the dangerous failure is often not CPU—it is an unbounded number of slow I/O operations. I use breaker + timeout + concurrency limits together.

Was this article useful?

Your feedback helps prioritize deeper technical content.

Romharshan Singh
ABOUT THE AUTHOR

Romharshan Singh

Senior Solution Architect and Full Stack Technology Leader with 20+ years of enterprise engineering experience across AI, cloud, distributed systems, Java, Node.js, React, Angular, Kafka and Kubernetes.