Cloud Design Patterns · Part 2 of 10 · October 18, 2026 · 7 min read

Circuit Breakers:
Failing Fast So a Slow Dependency Can't Take You Down

A slow dependency is more dangerous than a dead one. A circuit breaker with its own small bulkhead turns the first into the second on purpose.

A brushed steel electrical cabinet with rows of dark square switches, one lit by a small amber indicator, more cabinets receding in blue light.

Picture an order service with a pool of 200 request threads. Every checkout calls an address-validation API. The API normally answers in 80 milliseconds. One afternoon it starts answering in 30 seconds, because the vendor behind it is having a bad day.

Nothing is down. The API still returns 200s. It just returns them slowly.

At 50 checkouts a second, each holding a thread for 30 seconds, the service needs 1,500 threads and owns 200. The pool is empty in four seconds. The product pages never call address validation, but they share the pool, so they stop answering too. The load balancer's health checks time out, instances leave rotation, and the survivors take their traffic and drown faster.

This is a composite scenario, not one client's outage. Every number in it is arithmetic you can check.

The dependency was slow, not down, and the slow one did more damage. A dead dependency refuses the connection in milliseconds and the thread goes back to the pool. A slow one keeps the thread.

Why Slow Is Worse Than Down

A timeout limits how long one call waits. It does not limit how many calls are waiting at once.

The arithmetic is the one Lambda's documentation uses for concurrency: requests per second times seconds per request (Lambda concurrency). With a 30-second timeout, 50 requests a second need 1,500 threads. Cut the timeout to 5 seconds and you still need 250. Only a timeout of 4 seconds or less keeps the example inside a 200-thread pool, and that is before any retry multiplies the calls (post 1 covers that side of it). This post is part of the Cloud Design Patterns series.

The second problem is the shared pool. When every dependency draws from the same threads or connections, the slowest one sets the capacity of everything. The failure spreads to calls that had nothing to do with it.

Netflix wrote this down when it built Hystrix, its latency and fault-tolerance library. The Hystrix wiki says that under high-volume traffic, a single latent backend dependency can saturate all resources in seconds on all servers. It also describes the remedy: a small pool for each dependency, with requests rejected instead of queued when the pool is full.

AWS Prescriptive Guidance says the same thing in operations terms. Repeated retries against a callee that is timing out can cause network contention and database thread pool consumption, and with many users retrying they make the whole application worse (circuit breaker pattern).

A slow dependency is more dangerous than a dead one. The goal of a circuit breaker is to turn the first into the second on purpose.

How the Pattern Works

A circuit breaker sits between a caller and one dependency and has three states.

Closed. Calls pass through. The breaker counts failures and slow calls over a sliding window. A call that exceeds your latency budget counts as a failure even if it eventually succeeds, because to the thread it was one.

Open. When the failure rate in the window passes a threshold, the breaker opens. Calls now fail immediately, without touching the dependency, or return a fallback. The thread goes back to the pool at once instead of after seconds.

Half-open. After a cool-down, the breaker lets a few probe calls through. If they succeed, it closes. If one fails, it opens again for another cool-down.

Two details decide whether this works.

First, set a minimum number of calls before the breaker may trip. A window with four calls and two failures is a 50% failure rate and tells you nothing. Hystrix's defaults are a reasonable place to start: at least 20 requests in a 10-second rolling window, and a 50% error rate to trip (Hystrix configuration). They are not AWS numbers; tune them from your own traffic.

Second, give each dependency its own small pool or concurrency limit. This is the bulkhead, named for the watertight compartments in a ship's hull. The breaker needs time to see a failure and trip. The bulkhead limits the damage during that time. With 10 concurrent calls allowed to address validation, the worst case in the opening scenario is 10 stuck threads, not 200.

The fallback needs honesty. AWS's own guidance is to keep a degraded path much simpler than the primary one and to test it, and it adds that fallback strategies should generally be avoided (REL05-BP01). The Builders' Library explains why: a fallback is a mode the system enters only when everything else is going wrong, so it is the code least likely to have been exercised (Avoiding fallback in distributed systems). Often the honest answer is a fast, clear error. If you must fall back, use data that is already local; post 8 covers fallbacks that do not call a control plane.

failure rate over thresholdcool-down endsprobes succeeda probe failsBulkheadper-dependency call limitClosedcalls pass, failures countedOpenfail fast or fallbackHalf-opena few probe calls
A breaker moves between three states, and the bulkhead caps calls in flight while it decides.

Building It on AWS

AWS has no single managed circuit breaker. In application code you use a library: Resilience4j on the JVM, Polly on .NET, or pybreaker and opossum in Python and Node.js. Their state lives in process memory, which works for a service with long-lived instances. Lambda runs each concurrent request in its own execution environment (Lambda concurrency), so a breaker there needs shared state. Prescriptive Guidance keeps it in DynamoDB, and notes ElastiCache can replace DynamoDB for speed. The bulkhead is the part AWS gives you directly: Lambda reserved concurrency, and Amazon ECS Service Connect's outlier detection. Both are blunter than a library, and neither knows about fallbacks.

One naming trap: the Amazon ECS deployment circuit breaker is a different thing. It rolls back a failed deployment, and it does nothing about a slow dependency at runtime. AWS App Mesh once offered Envoy timeouts and circuit breaking, and it reached end of support on September 30, 2026 (App Mesh notice).

ServiceSettingValueSource
AWS Step Functions, Amazon DynamoDBOrchestrated breakerAn express workflow reads a CircuitStatus table; an unexpired record means open and the workflow fails at once. After retries with exponential backoff fail, it writes a record with an ExpiryTimeStamp, and DynamoDB TTL removes expired itemsCircuit breaker pattern
AWS LambdaReserved concurrency as a bulkheadDefault account limit of 1,000 concurrent executions per Region; 100 always stay unreserved, so 900 can be reserved by default; no charge; 0 throttles a function on purposeLambda concurrency, Configuring reserved concurrency
Amazon ECS Service ConnectOutlier detectionIf 5 or more connections to a task fail in 30 seconds, the proxy avoids that task for 30 to 300 secondsService Connect components
Amazon ECS Service ConnectRetries and timeout2 retries, the second avoiding the previous host; perRequestTimeout defaults to 15 seconds for HTTP, HTTP2, and gRPCService Connect components
Amazon ECSDeployment circuit breaker (not a runtime breaker)Default threshold type BOUNDED_PERCENT at 50% of desired count, clamped to 3 at the low end and 200 at the high endDeployment circuit breaker

Capping a payment adapter at 20 concurrent executions takes one command, from the same page (Configuring reserved concurrency):

aws lambda put-function-concurrency \
  --function-name payments-adapter \
  --reserved-concurrent-executions 20

When Not to Use It

A call with no fallback and no shared resource to protect. A nightly batch job that calls a slow API can simply wait. A breaker there adds code and little else. Put the work behind a queue and let the consumer take its time (post 3).

Low traffic. A breaker tuned on three calls a minute opens on noise. Either set a minimum call count, as above, or skip the breaker and rely on a well-chosen timeout and a retry budget.

One breaker shared across very different failure profiles. If one breaker guards every endpoint of a vendor, or every tenant, one bad endpoint or one noisy tenant opens it for all of them. Key breakers by dependency and operation, and give a tenant with a distinct profile its own.

A caller that is itself overloaded. A breaker protects you from a dependency. It does not shed your own excess load. For that, see load shedding and backpressure.

How We Check for It

In a Well-Architected Review, we look at this under the Reliability pillar, question REL05: how you design interactions in a distributed system to mitigate or withstand failures. The best practices behind it are REL05-BP01 (graceful degradation), REL05-BP04 (fail fast and limit queues), and REL05-BP05 (set client timeouts). In live accounts we look at:

  • A dependency inventory for the critical path, naming every synchronous call to a third party or another team's service.
  • An explicit connect timeout and request timeout on each call, not a framework default that may be infinite.
  • Separate pools or concurrency limits per dependency: reserved concurrency on Lambda functions, Service Connect settings, or code-level bulkheads.
  • A tested fallback or a deliberate fast failure for each dependency, with the behavior written down.
  • Breaker state changes emitted as metrics, with an alarm on a breaker that stays open.

These checks are part of the AWS Well-Architected Framework Review.

These patterns are what
a review checks for.

The AWS Well-Architected Framework Review measures your workload against these patterns in your live accounts, and the Remediation closes the High-Risk Issues it finds.

The next conversation

Let's define the result.

Tell Charles which challenge is in front of you. He will scope the work and make the result he will measure clear before you sign.

Book a 15-minute call with Charles

Remote engagements nationwide.
Serving clients since 2020.