npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

circuitbreaker-lambda

v1.0.1

Published

Circuit breaker pattern for AWS Lambda with DynamoDB state management

Readme

circuitbreaker-lambda

npm version license node TypeScript

Circuit breaker for AWS Lambda with distributed state. Unlike in-memory circuit breakers, circuitbreaker-lambda shares state across Lambda invocations and concurrent instances using DynamoDB, with support for custom state backends. Designed to fail open -- if the state provider is unavailable, requests pass through rather than failing.

| Path | Runtimes | How | When to use | | --- | --- | --- | --- | | npm package | Node.js 20+ | Import library or use Middy middleware | Programmatic control, Node.js only | | Lambda Layer | Any managed runtime | Add layer + env var | Any runtime, HTTP-based API |

Both paths share the same DynamoDB state schema -- functions using either approach share circuit state.

graph LR
  Event["Event
  (API GW, SQS,
  EventBridge, etc.)"]

  subgraph Lambda["AWS Lambda"]
    Handler --> CB["Circuit Breaker"]
    CB -- "CLOSED / HALF-OPEN" --> Downstream["Downstream
    service"]
    CB -- "OPEN" --> Fallback["Fallback
    function"]
    CB <--> DynamoDB["DynamoDB
    (shared state)"]
  end

  Event --> Handler
  Downstream --> Handler
  Fallback --> Handler

Table of Contents

Getting Started: npm Package

1. Install

npm install circuitbreaker-lambda

Requirements: Node.js >= 20

2. Create a DynamoDB table

aws dynamodb create-table \
  --table-name circuitbreaker-table \
  --attribute-definitions AttributeName=id,AttributeType=S \
  --key-schema AttributeName=id,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST

3. Set the environment variable

Add to your Lambda function:

CIRCUITBREAKER_TABLE=circuitbreaker-table

4. Add IAM permissions

Grant your Lambda execution role GetItem and UpdateItem on the table:

{
  "Effect": "Allow",
  "Action": ["dynamodb:GetItem", "dynamodb:UpdateItem"],
  "Resource": "arn:aws:dynamodb:*:*:table/circuitbreaker-table"
}

5. Wrap your calls

import { CircuitBreaker } from "circuitbreaker-lambda";

const circuitBreaker = new CircuitBreaker(unreliableFunction, {
  failureThreshold: 5,
  successThreshold: 2,
  timeout: 10000,
  fallback: fallbackFunction,
});

export const handler = async () => {
  const result = await circuitBreaker.fire();
  return { statusCode: 200, body: JSON.stringify(result) };
};

CommonJS:

const { CircuitBreaker } = require("circuitbreaker-lambda");

const circuitBreaker = new CircuitBreaker(unreliableFunction, options);

Using Middy?

If you use Middy (v4+), use the middleware variant instead of wrapping your calls. The middleware protects the entire handler -- when the circuit is OPEN, the handler is skipped:

import middy from "@middy/core";
import { circuitBreakerMiddleware } from "circuitbreaker-lambda/middy";

export const handler = middy(async (event) => {
  const data = await callDownstreamService();
  return { statusCode: 200, body: JSON.stringify(data) };
}).use(
  circuitBreakerMiddleware({
    circuitId: "downstream-api",
    failureThreshold: 3,
    fallback: async () => ({ statusCode: 503, body: "Service unavailable" }),
  })
);

The middleware uses before/after/onError hooks: before checks the circuit and short-circuits with the fallback if OPEN, after records success, onError records failure and rethrows.

Getting Started: Lambda Layer

The Lambda Layer enables circuit breaker protection with any Lambda runtime -- Node.js, Python, Java, .NET, Ruby. It runs a Rust-based extension as a local HTTP sidecar that your function calls.

1. Download the layer zip

Get circuitbreaker-lambda-layer-x86_64.zip or circuitbreaker-lambda-layer-aarch64.zip from the latest GitHub release.

2. Publish the layer to your account

aws lambda publish-layer-version \
  --layer-name circuitbreaker-lambda \
  --zip-file fileb://circuitbreaker-lambda-layer-aarch64.zip \
  --compatible-architectures arm64 \
  --region eu-west-1

3. Add the layer and environment variable

Add the layer ARN to your function and set CIRCUITBREAKER_TABLE. The extension binds its HTTP server and registers with the Lambda Extensions API during INIT -- Lambda won't invoke your handler until the extension is ready.

4. Call the local HTTP API from your function

The extension listens on http://127.0.0.1:4243 (configurable via CIRCUITBREAKER_PORT).

Node.js:

const resp = await fetch("http://127.0.0.1:4243/circuit/my-service");
const { allowed, state } = await resp.json();
if (!allowed) return { statusCode: 503, body: "Circuit OPEN" };

try {
  const result = await callDownstream();
  await fetch("http://127.0.0.1:4243/circuit/my-service/success", { method: "POST" });
  return { statusCode: 200, body: JSON.stringify(result) };
} catch (err) {
  await fetch("http://127.0.0.1:4243/circuit/my-service/failure", { method: "POST" });
  return { statusCode: 500, body: err.message };
}

Python:

import json, urllib.request

check = json.loads(urllib.request.urlopen("http://127.0.0.1:4243/circuit/my-service").read())
if not check["allowed"]:
    return {"statusCode": 503, "body": "Circuit OPEN"}

# ... call downstream, then POST /circuit/my-service/success or /failure

Layer HTTP API

| Endpoint | Method | Description | | --- | --- | --- | | /circuit/{id} | GET | Check circuit state. Returns {"allowed": true, "state": "CLOSED"} | | /circuit/{id}/success | POST | Record success. Returns {"state": "CLOSED"} | | /circuit/{id}/failure | POST | Record failure. Returns {"state": "OPEN"} | | /health | GET | Health check |

{id} must be 1-256 characters, using only alphanumeric characters, hyphens, underscores, dots, and colons (e.g., my-service, payments.api, tenant:123).

Supported Runtimes

The layer works with all supported Lambda runtimes:

  • Node.js: nodejs24.x, nodejs22.x, nodejs20.x
  • Python: python3.14, python3.13, python3.12, python3.11, python3.10
  • Java: java25, java21, java17, java11, java8.al2
  • .NET: dotnet10, dotnet8
  • Ruby: ruby3.4, ruby3.3, ruby3.2
  • Custom: provided.al2023, provided.al2 (Go, Rust, C++, etc.)

Both x86_64 and arm64 architectures are supported.

Memory and cold starts: Lambda allocates CPU proportionally to memory. At the default 128MB, cold starts are CPU-starved (AWS SDK initialization, TLS handshake, credential resolution). At 512MB, the layer adds ~350ms to cold start and warm invocations have near-zero overhead. The npm package adds ~50ms to cold start at 512MB. Both paths benefit from higher memory settings.

Circuit Breaker States

| State | Description | | --- | --- | | CLOSED | Normal operation. All calls pass through. | | OPEN | Requests fail immediately (or use fallback). Timeout uses exponential backoff. | | HALF-OPEN | A single probe request passes through. One failure immediately reopens. |

State Transitions

  • CLOSED -> OPEN: failureCount >= failureThreshold (within windowDuration)
  • OPEN -> HALF-OPEN: Timeout expires (nextAttempt <= Date.now())
  • HALF-OPEN -> OPEN: Any single failure (with exponential backoff on timeout)
  • HALF-OPEN -> CLOSED: successCount >= successThreshold

Configuration

Both the npm package and Lambda Layer support the same circuit breaker settings. The npm package uses constructor options; the layer uses environment variables.

| Setting | npm option | Layer env var | Default | Description | | --- | --- | --- | --- | --- | | Failure threshold | failureThreshold | CIRCUITBREAKER_FAILURE_THRESHOLD | 5 | Failed attempts before circuit opens | | Success threshold | successThreshold | CIRCUITBREAKER_SUCCESS_THRESHOLD | 2 | Successful attempts in HALF-OPEN before closing | | Timeout | timeout | CIRCUITBREAKER_TIMEOUT_MS | 10000 | Initial OPEN duration in ms | | Max timeout | maxTimeout | CIRCUITBREAKER_MAX_TIMEOUT_MS | 60000 | Max exponential backoff cap in ms | | Window duration | windowDuration | CIRCUITBREAKER_WINDOW_DURATION_MS | 60000 | Failure count reset window in ms |

npm-Only Options

| Option | Type | Default | Description | | --- | --- | --- | --- | | fallback | function \| null | null | Async fallback function for graceful degradation | | stateProvider | StateProvider | DynamoDBProvider | Custom state backend | | circuitId | string | AWS_LAMBDA_FUNCTION_NAME | Unique identifier for this circuit (required) | | cacheTtlMs | number | 0 | Warm invocation cache TTL in ms (0 = disabled) | | tableName | string | CIRCUITBREAKER_TABLE | DynamoDB table name (only with default provider) |

Layer-Only Options

| Variable | Default | Description | | --- | --- | --- | | CIRCUITBREAKER_PORT | 4243 | HTTP server port |

Environment Variables

| Variable | Used by | Description | | --- | --- | --- | | CIRCUITBREAKER_TABLE | Both | DynamoDB table name | | CIRCUITBREAKER_PORT | Layer | HTTP server port (default: 4243) | | CIRCUITBREAKER_FAILURE_THRESHOLD | Layer | Failures before circuit opens (default: 5) | | CIRCUITBREAKER_SUCCESS_THRESHOLD | Layer | Successes in HALF-OPEN before closing (default: 2) | | CIRCUITBREAKER_TIMEOUT_MS | Layer | Initial OPEN timeout in ms (default: 10000) | | CIRCUITBREAKER_MAX_TIMEOUT_MS | Layer | Max exponential backoff cap (default: 60000) | | CIRCUITBREAKER_WINDOW_DURATION_MS | Layer | Failure count reset window (default: 60000) |

Fail-Open Design

The circuit breaker is designed to never make things worse. If the state provider (DynamoDB or custom) is unavailable:

  • Read failures: The circuit assumes CLOSED and lets the request through.
  • Write failures: The response is returned normally; state loss is logged but does not affect the caller.

All provider errors are logged as structured JSON warnings:

{"source":"circuitbreaker-lambda","level":"warn","action":"getState","error":"Error: connection timeout"}

Concurrent Instances and Distributed State

Circuit breaker state in DynamoDB uses unconditional writes (last writer wins). This means concurrent Lambda instances can race when updating state:

  • HALF-OPEN state probe races: If two instances both read HALF-OPEN state and succeed, each increments successCount against its own in-memory copy. The second write overwrites the first, potentially delaying the CLOSED transition.
  • Failure count races: Two instances both reading failureCount: 4 and failing will both write failureCount: 5 and open the circuit. This is safe (idempotent).

In practice, these races are benign -- the circuit may be slightly slower to close than expected, but it will never miss an open. For stricter guarantees, implement a custom StateProvider with DynamoDB conditional writes or transactions.

State Schema

State records use the following schema, shared between the npm package and the Lambda Layer extension:

| Field | Type | Description | | --- | --- | --- | | id | string | Partition key (circuit identifier) | | circuitState | string | CLOSED, OPEN, or HALF-OPEN | | failureCount | number | Current failure count | | successCount | number | Current success count (in HALF-OPEN state) | | nextAttempt | number | Epoch ms when OPEN circuit transitions to HALF-OPEN | | lastFailureTime | number | Epoch ms of last failure (for window-based reset) | | consecutiveOpens | number | Consecutive HALF-OPEN->OPEN transitions (for exponential backoff) | | stateTimestamp | number | Epoch ms of last state write | | schemaVersion | number | Schema version (currently 1) |

Advanced Usage

Custom State Provider

The default DynamoDBProvider works for most cases. For testing or custom backends, implement the StateProvider interface:

import { CircuitBreaker, MemoryProvider } from "circuitbreaker-lambda";

// For testing (no DynamoDB needed)
const breaker = new CircuitBreaker(myFunction, {
  stateProvider: new MemoryProvider(),
  circuitId: "my-circuit",
});

Bring your own backend (Redis, MemoryDB, etc.):

import { CircuitBreaker } from "circuitbreaker-lambda";
import type { StateProvider, CircuitBreakerState } from "circuitbreaker-lambda";

class RedisProvider implements StateProvider {
  async getState(circuitId: string): Promise<CircuitBreakerState | undefined> { /* ... */ }
  async saveState(circuitId: string, state: CircuitBreakerState): Promise<void> { /* ... */ }
}

const breaker = new CircuitBreaker(myFunction, {
  stateProvider: new RedisProvider(),
  circuitId: "my-circuit",
});

Low-Level API

For custom integration patterns (middleware, decorators, etc.), use check(), recordSuccess(), and recordFailure() instead of fire(). Pass null as the request function:

const breaker = new CircuitBreaker(null, { circuitId: "my-api", stateProvider: provider });

const allowed = await breaker.check();
if (!allowed) {
  return fallbackResponse;
}
try {
  const result = await callDownstream();
  await breaker.recordSuccess();
  return result;
} catch (err) {
  await breaker.recordFailure();
  throw err;
}

recordSuccess() and recordFailure() must be called after check() in the same invocation -- they operate on the state loaded by check().

Warm Invocation Caching

Opt-in caching reduces DynamoDB reads on warm Lambda invocations by serving state from an instance-level cache within the TTL window:

const breaker = new CircuitBreaker(myFunction, {
  cacheTtlMs: 200, // serve cached state for 200ms before re-fetching
});

Note: Caching trades consistency for latency. Concurrent Lambda instances may serve stale state for up to cacheTtlMs. In the worst case, stale cached state can suppress the CLOSED->OPEN transition if multiple instances read cached failureCount and write back independently. Default is 0 (disabled). Only enable caching if you've measured the DynamoDB overhead and need to reduce it.

Examples

Both the npm package and Lambda Layer include examples with a toggleable downstream service that lets you walk through the full circuit breaker lifecycle.

npm Package Example

cd examples/sam
npm install
sam build
sam deploy --guided

| Endpoint | Description | | --- | --- | | GET / | Call downstream through the circuit breaker | | GET /status | Show current circuit state and downstream health | | POST /toggle | Flip downstream between healthy and unhealthy |

Walkthrough:

# 1. Requests succeed (circuit CLOSED)
curl $API_URL/

# 2. Make downstream unhealthy
curl -X POST $API_URL/toggle

# 3. Repeat — watch failures accumulate, then circuit opens with fallback
curl $API_URL/
curl $API_URL/
curl $API_URL/

# 4. Check circuit state
curl $API_URL/status

# 5. Make downstream healthy again
curl -X POST $API_URL/toggle

# 6. Wait 15 seconds for the timeout, then call again
#    Circuit transitions HALF-OPEN -> CLOSED as requests succeed
curl $API_URL/
curl $API_URL/

Lambda Layer Example

cd examples/layer
sam build
sam deploy --guided

To build the layer from source instead of downloading, see layer/build.sh.

Same workflow as above, with Node.js endpoints at /node, /node/status, /node/toggle and Python at /python, /python/status, /python/toggle.

Minimal Examples

If you just want to see the bare minimum code, without the toggle/status test infrastructure:

  • examples/minimal-npm/ — npm package protecting a real API call (22 lines of handler code)
  • examples/minimal-layer/ — Lambda Layer protecting the same API call (25 lines of handler code)

Acknowledgments

Inspired by Michael Nygard's book Release It!, Martin Fowler's article on the circuit breaker, and Mark Michon's post on building a Node.js circuit breaker.

Changelog

See CHANGELOG.md for a detailed list of changes.

License

MIT