{"id":22496672,"url":"https://github.com/kaiosilveira/nodejs-circuit-breaker","last_synced_at":"2026-05-16T01:31:31.527Z","repository":{"id":59262333,"uuid":"529858449","full_name":"kaiosilveira/nodejs-circuit-breaker","owner":"kaiosilveira","description":"Sample circuit breaker implementation on top of express. Inspired on the Circuit Breaker pattern description using the Leaky Bucket strategy for thresholds as described in the \"Release It! - Nygard\" book","archived":false,"fork":false,"pushed_at":"2022-10-15T11:53:39.000Z","size":219,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-09-11T14:50:50.867Z","etag":null,"topics":["nodejs","stability-patterns"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kaiosilveira.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-08-28T12:39:49.000Z","updated_at":"2023-04-14T23:48:08.000Z","dependencies_parsed_at":"2022-09-13T23:02:41.366Z","dependency_job_id":null,"html_url":"https://github.com/kaiosilveira/nodejs-circuit-breaker","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/kaiosilveira/nodejs-circuit-breaker","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kaiosilveira%2Fnodejs-circuit-breaker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kaiosilveira%2Fnodejs-circuit-breaker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kaiosilveira%2Fnodejs-circuit-breaker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kaiosilveira%2Fnodejs-circuit-breaker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kaiosilveira","download_url":"https://codeload.github.com/kaiosilveira/nodejs-circuit-breaker/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kaiosilveira%2Fnodejs-circuit-breaker/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33087028,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-15T20:25:35.270Z","status":"ssl_error","status_checked_at":"2026-05-15T20:25:34.732Z","response_time":103,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["nodejs","stability-patterns"],"created_at":"2024-12-06T20:13:38.401Z","updated_at":"2026-05-16T01:31:31.512Z","avatar_url":"https://github.com/kaiosilveira.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Continuous Integration](https://github.com/kaiosilveira/nodejs-circuit-breaker/actions/workflows/ci.yml/badge.svg)](https://github.com/kaiosilveira/nodejs-circuit-breaker/actions/workflows/ci.yml)\n\n_This repository is an example implementation in NodeJS of a Circuit Breaker as described in \"Release it! - Nygard\" and is part of my Stability Patterns Series. Check out [kaiosilveira/stability-patterns](https://github.com/kaiosilveira/stability-patterns) for more details._\n\n# Circuit breaker\n\nIn electrical engineering there's a concept of \"circuit breaker\", a mechanism to stop energy of flowing through and potentially burning down all the system if something goes wrong. Its equivalent in the software engineering world mimics this mechanism and creates a parallel between energy and requests. As Nygard mentions in his book: requests are the energy of our system, and we need a way of protecting the whole system of being completely burned down.\n\nCircuit breakers contains three states:\n\n- `CLOSED`: Requests flow normally through\n- `OPEN`: Requests do not flow through and operation is refused\n- `HALF_OPEN`: Next request is evaluated by the circuit breaker code, if it succeeds, the circuit state is changed to `CLOSED`, otherwise it goes back to `OPEN`\n\nFind below the hypothetical domain used to give an example of this pattern in action, alongside all of its technical details.\n\n## Hypothetical domain\n\nA banking app was chosen to be the hypothetical domain for this implementation. This app fetches transaction history data from the `TransactionHistoryService`, which is in a remote location. The circuit breaker was implemented to protect the app from displaying errored transaction pages to the users if the transaction history service is down. In such cases, the circuit breaker will return the last cached result for that specific user, only returning an error if there is a cache miss.\n\n## Implementation details\n\nThis implementation posed some interesting technical challenges, specially on handling the decrementing of the counters, as node is a single-thread language. The solution for the counters was to abstract it into a [child process](./src/leaky-bucket/index.ts), which wraps a [LeakyBucket](./src/leaky-bucket/leaky-bucket/index.ts) class that holds the counters for all registered clients. The registration is done via inter-process communication using event emitters, and the counters are infinitely decremented using a `setInterval` loop, with a minimum of `0` for each counter:\n\n```typescript\nMath.max(0, this.COUNTERS[subscriptionId].current - 1);\n```\n\n### Tech stack\n\n**Programming language**\n\nTypescript was chosen to be the programming language for this project, mainly because it allows for a more formal object-oriented programming approach on top of Javascript, which gave me the right level of flexibility when using abstractions (in the form of interfaces), but also because of the improved development experience, with fast feedback loops (code -\u003e build -\u003e unit test) and IDE support (using VSCode).\n\n**Web server**\n\nExpress was chosen as the web server framework for this implementation, its middleware structure allow us to plug our circuit breaker as a middleware, blocking requests when in the **_OPEN_** state.\n\n**Test framework**\n\nJest was chosen as the test runner. Its built-in spying and stubbing structure allows for fast implementation of unit tests and its declarative expectation syntax allows for a readable and elegant test structure.\n\n### Test strategy\n\nAs per default, unit tests were implemented for all files that contain logic.\n\n### Continuous Integration\n\nA continuous integration pipeline was put in place to make sure all additions to the `main` branch are healthy. The pipeline definition is pretty straightforward:\n\n```yml\nname: Continuous Integration\n\non:\n  push:\n    branches: [main]\n\njobs:\n  Integration:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [16.x]\n\n    steps:\n      - name: Check out the repository\n        uses: actions/checkout@v2\n\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v1\n        with:\n          node-version: ${{ matrix.node-version }}\n\n      - name: Install dependencies\n        run: npm install\n\n      - name: Build project\n        run: npm run build\n\n      - name: Run unit tests\n        run: npm run test:unit\n```\n\nIt install all the dependencies, build the project and run all unit tests.\n\n### The Circuit Breaker\n\nThe `CircuitBreaker` interface is relatively simple (at least in its surface):\n\n```typescript\nexport default interface CircuitBreaker {\n  open(): void;\n  halfOpen(): void;\n  close(): void;\n  registerFailure(): void;\n}\n```\n\nThe `open`, `halfOpen` and `close` methods allow for changing the circuit breaker state, and the `registerFailure` method implements the logic to compute a new failure, in whatever way the client code likes.\n\nIn our particular case, an [ExpressCircuitBreaker](./src/circuit-breaker/express/index.ts) was implemented. This implementation contains a `monitor(req: Request, res: Response, next: Function)` method, which has the signature of an express middleware and is meant to be added into an express middleware chain. This method is one of the key parts of this implementation, it fails fast in case the circuit is `OPEN`, and adds a listener into the `response.finish` event to monitor each outgoing response and check its status.\n\n`ExpressCircuitBreaker` also relies heavily on the [State Pattern](https://github.com/kaiosilveira/design-patterns/tree/main/state) to react to events when in different states without resorting to a many `switch` statements.\n\n#### Closed circuit, requests flowing though\n\nAt bootstrap time, the circuit breaker sets its status to closed:\n\n```typescript\nconstructor({ bucket, logger, config }: ExpressCircuitBreakerProps) {\n    // more initialization code here\n    this.state = new CircuitBreakerClosedState({ circuitBreaker: this, logger: this.logger });\n// ...\n}\n```\n\nAs in a electrical system, a closed circuit means that energy is allowed to flow through. In our case, energy = requests. As mentioned, the circuit breaker monitors all outgoing responses, \"taking notes\" on its status code and reacting accordingly. In this implementation, this monitoring is done by listening to the `finish` event on the response object:\n\n```typescript\nmonitor(_: Request, res: Response, next: Function): void | Response {\n  // rest of the code above here\n  res.on('finish', () =\u003e {\n    switch (res.statusCode) {\n      case 200:\n        this.state.handleOkResponse();\n        break;\n      case 500:\n        this.state.handleInternalServerErrorResponse();\n        break;\n      default:\n        break;\n    }\n  });\n\n  next();\n}\n```\n\nIn the code above, the `handleOkResponse()` method will simply log the fact that there was a successful response going through. For the `handleInternalServerErrorResponse()`, though, things are a little bit more involved. When requests fail and the circuit is `CLOSED`, the `registerFailure()` method is invoked. This method sends a message to the `LeakyBucket` process, which in turn will increase the failure count for the circuit breaker bucket, based on its identifier:\n\n```typescript\nclass ExpressCircuitBreaker {\n  // lots of code\n\n  registerFailure() {\n    this.bucket.send({\n      type: 'NEW_FAILURE',\n      payload: { subscriptionId: this.subscriptionId },\n    } as LeakyBucketMessage);\n  }\n  // more code\n}\n```\n\n```typescript\nclass LeakyBucketProcessManager {\n  // ...some code...\n\n  private handleNewFailureMessage(msg: LeakyBucketMessage): void {\n    const { subscriptionId } = msg.payload;\n    this._bucket.increment({ subscriptionId });\n    // some other code ...\n  }\n\n  // more code...\n}\n```\n\nThis happens until the bucket starts leaking, i.e., when the failure count is high enough to go above the specified threshold, which will cause the bucket to report a `THRESHOLD_VIOLATION` event back to the main process:\n\n```typescript\nclass LeakyBucketProcessManager {\n  // ...some code...\n  private handleNewFailureMessage(msg: LeakyBucketMessage): void {\n    // some other code ...\n    if (this._bucket.isAboveThreshold({ subscriptionId })) {\n      this._processRef.send?.({\n        type: LeakyBucketMessageTypes.THRESHOLD_VIOLATION,\n        payload: { subscriptionId },\n      });\n    }\n  }\n\n  // more code...\n}\n```\n\nThe `CircuitBreaker` is able to listen to this event because it subscribes to it at startup time:\n\n```typescript\nthis.bucket.on('message', this._handleBucketMessage);\n```\n\nAnd then it reacts accordingly, opening the circuit:\n\n```typescript\nclass ExpressCircuitBreaker extends EventEmitter implements CircuitBreaker {\n  private _handleBucketMessage(msg: LeakyBucketMessage): void {\n    switch (msg.type) {\n      // code\n      case 'THRESHOLD_VIOLATION':\n        this.logger.info({ msg: 'Threshold violated. Opening circuit.' });\n        this.open();\n        break;\n      // ...\n    }\n  }\n\n  // code...\n}\n```\n\n#### External service is misbehaving, circuit is opened\n\nWhen the circuit is `OPEN`, all incoming requests which would activate the problematic remote service will be rejected, unless there is a cache entry for the user requesting the data.\n\n##### Cache entry available\n\nIn this case, the circuit breaker will return a potentially outdated result, but which will still allow users to see something on the UI. to let users know about the possible discrepancy between what's in the screen and what is the current state of the system, a flag `fromCache: true` is provided.\n\n##### Failing fast\n\nOne of the main reasons to implement a Circuit Breaker is to be able to fail fast if we know the request is likely to fail anyway:\n\n```typescript\nif (this.state.status === CircuitBreakerStatus.OPEN) {\n  this.logger.info({ msg: 'Call refused from circuit breaker', status: this.state.status });\n  return res.status(500).json({ msg: 'Call refused from circuit breaker' });\n}\n```\n\nA log is added to let our monitoring team know that a circuit breaker was opened, and a `500 INTERNAL SERVER ERROR` response is returned to the client.\n\n#### Control levels goes below threshold, circuit moves to half-open\n\nAfter a while, the bucket will stop leaking, the `counter` for the given circuit breaker will be back below the threshold, and whenever it happens, the bucket process itself will notify this fact:\n\n```typescript\nif (currentCount - threshold === 1) {\n  process.send?.({ type: 'THRESHOLD_RESTORED', subscriptionId });\n}\nbucket.decrement({ subscriptionId });\n```\n\nThis will in turn trigger a listener on the circuit breaker, which will react changing its status to `HALF_OPEN`:\n\n```typescript\nclass ExpressCircuitBreaker extends EventEmitter implements CircuitBreaker {\n  // ...code\n  private _handleBucketMessage(msg: LeakyBucketMessage): void {\n    switch (msg.type) {\n      // other case statements...\n      case 'THRESHOLD_RESTORED':\n        this.halfOpen();\n        this.logger.info({\n          msg: 'Threshold restored. Moving circuit to half-open.',\n          status: this.state.status,\n        });\n        break;\n      // ...\n    }\n  }\n  // more code\n}\n```\n\nIn the `HALF_OPEN` state, the next response decides wether new requests will be allowed to flow though again (if `statusCode: 200`) or will continue being denied (if `statusCode: 500`).\n\n### The Leaky Bucket\n\nThe `LeakyBucket` class controls the lifecycle of the counters for a given `subscriptionId`. As mentioned above, this class is used by a process manager that runs as a child-process and communicates to the main process whenever needed. The `LeakyBucket` interface is pretty straightforward:\n\n```typescript\ninterface LeakyBucket {\n  subscribe({ subscriptionId, threshold }: { subscriptionId: string; threshold?: number }): void;\n  fetchThresholdFor({ subscriptionId }: { subscriptionId: string }): number;\n  fetchCountFor({ subscriptionId }: { subscriptionId: string }): number;\n  increment({ subscriptionId }: { subscriptionId: string }): void;\n  decrement({ subscriptionId }: { subscriptionId: string }): void;\n  resetCountFor({ subscriptionId }: { subscriptionId: string }): void;\n  isAboveThreshold({ subscriptionId }: { subscriptionId: string }): Boolean;\n}\n```\n\nAnd the corresponding `LeakyBucketProcessManager` looks also pretty simple:\n\n```typescript\ninterface LeakyBucketProcessManager {\n  handleMessage(msg: LeakyBucketMessage): void;\n  handleTick(): void;\n  getTickIntervalInMs(): number;\n}\n```\n\nTo start the magic, we need to create a new instance of `LeakyBucketProcessManager` and give it a `LeakyBucket`, a value for `tickIntervalMs` and a `ref` to the process:\n\n```typescript\nconst processManager = new LeakyBucketProcessManagerImpl({\n  processRef: process,\n  bucket: new LeakyBucketImpl(),\n  tickIntervalMs: 1000,\n});\n```\n\nThen, we need to plug its `handleMessage` method into the `process.on('message', fn)` so we can listen to messages from the main process:\n\n```typescript\nprocess.on('message', processManager.handleMessage);\n```\n\nThe `handleMessage` method knows how to manipulate the `LeakyBucket` instance according to the type of message received. It looks like this:\n\n```typescript\nhandleMessage(msg: LeakyBucketMessage): void {\n  switch (msg.type) {\n    case LeakyBucketMessageTypes.REGISTER:\n      this.handleRegisterMessage(msg);\n      break;\n    case LeakyBucketMessageTypes.NEW_FAILURE:\n      this.handleNewFailureMessage(msg);\n      break;\n    case LeakyBucketMessageTypes.RESET:\n      this.handleResetMessage(msg);\n      break;\n    default:\n      break;\n  }\n}\n```\n\nFinally, we can set up the infinity loop using a `setInterval` statement to decrement the counters in an interval of `tickIntervalMs`:\n\n```typescript\nsetInterval(processManager.handleTick, processManager.getTickIntervalInMs());\n```\n\nThe code above basically means that for every tick of the interval we will be decrementing `1` from the counters of each subscription. This is done inside the `handleTick` function, which does a few things:\n\n- goes over each subscription\n  - decrements it\n  - checks if the current `counter - threshold` is equal `1`\n  - if so, send a message to the main process notifying that the control level was restored\n\nBelow there's the actual implementation:\n\n```typescript\nhandleTick(): void {\n  this._bucket.fetchSubscriptionIds().forEach((subscriptionId: string) =\u003e {\n    const currentCount = this._bucket.fetchCountFor({ subscriptionId });\n    const threshold = this._bucket.fetchThresholdFor({ subscriptionId });\n    if (currentCount - threshold === 1) {\n      this._processRef.send?.({\n        type: LeakyBucketMessageTypes.THRESHOLD_RESTORED,\n        subscriptionId,\n      });\n    }\n    this._bucket.decrement({ subscriptionId });\n  });\n}\n```\n\n### Faking a remote service having trouble\n\nTo fake a `TransactionHistoryService` under trouble, we're going to use our [TransactionHistoryService](src/app/services/transaction-history/fake/index.ts). This fake returns a failure in every fourth response if the circuit breaker status is not `OPEN`. With this, we can simulate \"random\" failures from a remote service and can test that our circuit breaker is behaving as expected.\n\n### Application state\n\nTo keep track of all registered circuit breakers and to allow for direct manipulation of their states, an `ApplicationState` interface was defined:\n\n```typescript\ninterface ApplicationState {\n  fetchCircuitBreakerState(circuitBreakerId: string): CircuitBreakerStatus;\n  registerCircuitBreaker(circuitBreaker: CircuitBreaker): void;\n  describeRegisteredCircuitBreakers(): Array\u003cCircuitBreakerDescription\u003e;\n  setCircuitBreakerState({\n    circuitBreakerId,\n    state,\n  }: {\n    circuitBreakerId: string;\n    state: CircuitBreakerStatus;\n  }): void;\n}\n```\n\nand an [in-memory representation](./src/app/infra/application-state/in-memory/index.ts) was provided.\n\nThis implementation acts as a middleman between the `admin` resource and the `CircuitBreaker`s themselves, allowing for HTTP requests to modify the current state of any circuit breaker.\n\n### Taking the kid to the playground\n\nTo \"manually\" test our circuit breaker, we are going to use `loadtest`. With that, we will simply send 10 requests per second targeting the transaction history endpoint, which will eventually cause the fake service mentioned above to fail often enough to trip the circuit breaker:\n\n```console\nloadtest --rps 10 'http://localhost:3000/transaction-history/mine'\n```\n\nWe can see in the logs that the circuit breaker opens after some failed requests, then it starts to resolve requests from cache and, after a while, it recovers to `HALF_OPEN`, allowing new requests to flow trough:\n\n```console\ninfo: {\"msg\":\"Successful response\",\"status\":\"closed\"} {\"defaultMeta\":{\"object\":\"ExpressCircuitBreaker\"}}\ninfo: {\"msg\":\"Successful response\",\"status\":\"closed\"} {\"defaultMeta\":{\"object\":\"ExpressCircuitBreaker\"}}\nerror: {\"msg\":\"Failed to execute\"} {\"defaultMeta\":{\"object\":\"ExpressCircuitBreaker\"}}\ninfo: {\"msg\":\"Threshold violated. Opening circuit.\"} {\"defaultMeta\":{\"object\":\"ExpressCircuitBreaker\"}}\ninfo: {\"msg\":\"Resolving request using cached data while in OPEN state\"} {\"defaultMeta\":{\"object\":\"ExpressCircuitBreaker\"}}\ninfo: {\"msg\":\"Resolving request using cached data while in OPEN state\"} {\"defaultMeta\":{\"object\":\"ExpressCircuitBreaker\"}}\ninfo: {\"msg\":\"Resolving request using cached data while in OPEN state\"} {\"defaultMeta\":{\"object\":\"ExpressCircuitBreaker\"}}\ninfo: {\"msg\":\"Resolving request using cached data while in OPEN state\"} {\"defaultMeta\":{\"object\":\"ExpressCircuitBreaker\"}}\ninfo: {\"msg\":\"Resolving request using cached data while in OPEN state\"} {\"defaultMeta\":{\"object\":\"ExpressCircuitBreaker\"}}\ninfo: {\"msg\":\"Resolving request using cached data while in OPEN state\"} {\"defaultMeta\":{\"object\":\"ExpressCircuitBreaker\"}}\ninfo: {\"msg\":\"Threshold restored. Moving circuit to half-open.\",\"status\":\"half_open\"} {\"defaultMeta\":{\"object\":\"ExpressCircuitBreaker\"}}\ninfo: {\"msg\":\"Successful response while in a HALF_OPEN state. Closing the circuit.\",\"status\":\"half_open\"} {\"defaultMeta\":{\"object\":\"ExpressCircuitBreaker\"}}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkaiosilveira%2Fnodejs-circuit-breaker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkaiosilveira%2Fnodejs-circuit-breaker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkaiosilveira%2Fnodejs-circuit-breaker/lists"}