Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Circuit Breaker
The pattern that stops one failing dependency from taking down the fleet: trip after consecutive failures, fail fast while open, probe once after a cooldown. Implement the closed → open → half-open state machine as a pure replay.
The problem
Implement a circuit breaker as a pure replay: given threshold (consecutive failures that trip it), cooldown (seconds it stays open), and calls — a non-decreasing list of [time, succeeds] pairs describing calls and whether the dependency would succeed — return one outcome per call: "ok", "fail", or "rejected". Rules: while closed, calls go through; a success resets the consecutive-failure count, a failure increments it, and reaching threshold trips the breaker open at that call’s time. While open, calls before opened_at + cooldown are "rejected" without touching the dependency. The first call at or after that moment is a half-open probe: it goes through — success closes the breaker (count reset), failure re-opens it from the probe’s time.
Your turn — write it
Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.
Implement circuit_breaker(threshold, cooldown, calls) → list of "ok" / "fail" / "rejected", replaying the closed → open → half-open state machine.
Examples and constraints 4 examples
threshold = 2, cooldown = 10, calls = [[0,false],[1,false],[2,true],[11,true],[12,true]]["fail","fail","rejected","ok","ok"]threshold = 1, cooldown = 10, calls = [[0,false],[10,false],[15,true],[20,true]]["fail","fail","rejected","ok"]threshold = 2, cooldown = 10, calls = [[0,false],[1,true],[2,false],[3,true]]["fail","ok","fail","ok"]threshold = 1, cooldown = 10, calls = [[0,false],[9.999,true],[10,true]]["fail","rejected","ok"]- Outcomes are exactly "ok", "fail", or "rejected".
- The count is of consecutive failures; any success while closed resets it to 0.
- Rejected calls never touch the dependency and do not change the failure count.
- The half-open probe is a single call; its result alone decides closed vs re-opened.
Hints 4 ideas
- Track three things: state (closed/open), consecutive failure count, and when the breaker opened.
- Open + time < opened_at + cooldown → "rejected", and nothing else changes.
- Open + time ≥ opened_at + cooldown → treat as the half-open probe: run the call, then either close (success) or re-open with opened_at = this time.
- Trip check happens after counting a closed-state failure: count == threshold → open, opened_at = now.
Approach, complexity & discussion — open after you solve
Start from brute force
The obvious first version has two states. Count consecutive failures, open at threshold, reject until opened_at + cooldown, then flip straight back to closed with the count at 0. It costs O(1) time per call and O(1) space, and it even passes the first example. It is wrong when the dependency is still down after the cooldown: every caller rushes back in, and the breaker needs threshold fresh failures before it trips again. With threshold = 2, cooldown = 10 and failures at t = 0, 1, 11 and 12, it returns four "fail"s, while the spec sends one probe at t = 11 and rejects t = 12.
The approach
Keep exactly the reference’s three variables: state ("closed" or "open"), fails (consecutive failures while closed) and opened_at. Half-open is not stored. It is the moment an open breaker’s cooldown has expired, which the reference detects as probing = state == "open" once the rejection check has passed.
Every call takes one of three paths. Open with t < opened_at + cooldown: append "rejected" and change nothing. A success, whether closed or probing: append "ok", set fails = 0 and state = "closed". A failure: when probing, only move opened_at to t, which starts a full new cooldown; when closed, increment fails and open at t once it reaches threshold. The invariant: the dependency is called only while closed, or for exactly one probe per expired cooldown.
Walk through an example
On the first example (threshold = 2, cooldown = 10) the breaker trips at t = 1, rejects one call, and is healed by the probe at t = 11.
| step | call (t, succeeds) | state before | outcome | fails | opened_at | state after |
|---|---|---|---|---|---|---|
| 1 | [0, false] | closed | "fail" | 1 | 0 | closed |
| 2 | [1, false] | closed | "fail" | 2, trips | 1 | open |
| 3 | [2, true] | open, 2 < 11 | "rejected" | 2 | 1 | open |
| 4 | [11, true] | open, 11 ≥ 11: probe | "ok" | 0 | 1 | closed |
| 5 | [12, true] | closed | "ok" | 0 | 1 | closed |
Complexity
The replay is O(n) time for n calls, O(1) per call (one comparison and a few assignments), and O(1) extra space beyond the output list. The two-state version has the same cost; its problem is correctness, not speed. A live breaker shared across threads is still O(1) per call, but its state must be updated atomically.
Edge cases to test
- A call exactly at
opened_at + cooldownis the probe, and one just before it is not:circuit_breaker(1, 10, [[0, False], [9.999, True], [10, True]])returns["fail", "rejected", "ok"], because the check is a strictt < opened_at + cooldown. - A failed probe restarts the cooldown from the probe’s own time:
circuit_breaker(1, 10, [[0, False], [10, False], [15, True], [20, True]])returns["fail", "fail", "rejected", "ok"]; t = 15 is rejected because the window now ends at 20. - Failures split by a success never trip:
circuit_breaker(2, 10, [[0, False], [1, True], [2, False], [3, True]])returns["fail", "ok", "fail", "ok"]. - Rejections change nothing: with
threshold = 1, cooldown = 100, calls at t = 50 and t = 99 that would succeed are both"rejected", and neither touchesfailsoropened_at. - A cooldown of 0 makes the very next call the probe:
circuit_breaker(1, 0, [[0, False], [0, True], [0, True]])returns["fail", "ok", "ok"]. An emptycallslist returns[].
Common mistakes
- Not resetting the failure count on success, so the breaker trips on unrelated intermittent errors.
- Skipping the half-open trial — the breaker then either sticks open forever or flaps between states.
- Incrementing
failson a failed probe but leavingopened_atat the original trip: the cooldown has already expired, so the very next call is probed again instead of waiting a full cooldown. - Using
<=in the cooldown check, which rejects the call landing exactly onopened_at + cooldowninstead of probing it.
Follow-ups an interviewer asks next
- Which errors should count as failures?
- Only the ones that say the dependency is unhealthy: timeouts, connection errors and 5xx responses. A 4xx such as 404 or 422 means this request was wrong, not that the service is down, and counting it lets one buggy caller trip the breaker for everyone. A 429 is a health signal too, but it usually calls for backing off, which a retry policy handles.
- How would you trip on a failure rate instead of a consecutive count?
- Keep a sliding window of the last N calls, or of the last T seconds, and trip when the failure ratio crosses a threshold, but only once the window holds a minimum number of calls, so two failures out of two calls do not read as 100%. resilience4j configures its breakers this way: a window type and size, a failure-rate threshold and a minimum number of calls.
- What should half-open do when many requests arrive at once?
- Admit a fixed number of trial calls and reject the rest until those finish, so a recovering dependency is not hit with the full load at once. In a concurrent service that takes an atomic compare-and-set on the state; otherwise every thread that sees the expired cooldown sends its own probe. Some designs also require several successful probes before closing.
- The service runs on 50 instances. Shared breaker or one per instance?
- Usually one per instance. The state is tiny, decisions stay local and fast, and a dependency failing for one instance (a bad network path) may be fine for the others. A shared breaker in Redis gives fleet-wide agreement, but it adds a network hop to every call and a new dependency that can fail on its own.
Where this shows up
The circuit breaker stops a failing dependency from taking your service down with it: fail fast so threads are not parked on timeouts, and give the sick dependency room to recover. It is a staple of resilient microservices (Hystrix, resilience4j), used alongside retries-with-backoff and bulkheads.
Reference solution
Reference solution — Python
def circuit_breaker(threshold, cooldown, calls):
out = []
state = 'closed'
fails = 0
opened_at = 0.0
for t, ok in calls:
if state == 'open' and t < opened_at + cooldown:
out.append('rejected')
continue
# closed, or the half-open probe
probing = state == 'open'
if ok:
out.append('ok')
fails = 0
state = 'closed'
else:
out.append('fail')
if probing:
opened_at = t
else:
fails += 1
if fails >= threshold:
state = 'open'
opened_at = t
return out
print(circuit_breaker(2, 10, [[0, False], [1, False], [2, True], [11, True], [12, True]]))Reference solution — TypeScript
type Outcome = 'ok' | 'fail' | 'rejected';
function circuitBreaker(threshold: number, cooldown: number, calls: [number, boolean][]): Outcome[] {
const out: Outcome[] = [];
let state: 'closed' | 'open' = 'closed';
let fails = 0;
let openedAt = 0;
for (const [t, ok] of calls) {
if (state === 'open' && t < openedAt + cooldown) {
out.push('rejected');
continue;
}
const probing = state === 'open';
if (ok) {
out.push('ok');
fails = 0;
state = 'closed';
} else {
out.push('fail');
if (probing) {
openedAt = t;
} else {
fails += 1;
if (fails >= threshold) {
state = 'open';
openedAt = t;
}
}
}
}
return out;
}
console.log(circuitBreaker(2, 10, [[0, false], [1, false], [2, true], [11, true], [12, true]]));Questions about Circuit Breaker
What is the half-open state in a circuit breaker?
Half-open is the trial period after the cooldown. The breaker has been rejecting calls to give the dependency time to recover, and it needs evidence before letting traffic back, so it lets one call through as a probe. If the probe succeeds, the breaker closes and normal traffic resumes with the failure count at zero. If it fails, the breaker opens again and a full new cooldown starts from the probe’s time. In this challenge half-open is not a stored state: it is simply the first call that arrives at or after opened_at plus cooldown.
Why count consecutive failures instead of total failures?
A total count never forgets. A service that fails one request in a thousand would eventually reach any threshold and trip, even though it is healthy. Counting consecutive failures and resetting on every success asks a sharper question: has the dependency failed every recent call? That is strong evidence it is down rather than flaky. The trade-off is that a dependency failing most of its calls can avoid tripping if an occasional success keeps resetting the count, which is why many production breakers use a failure rate over a sliding window instead.
What is the difference between a circuit breaker and retries?
Retries assume a failure is brief and try again, which helps with a dropped connection or one slow request. A circuit breaker assumes repeated failures mean the dependency is down, and it stops calling it for a while. They work together: retry a few times with backoff, and let the breaker stop the retries when everything is failing. Without a breaker, retries multiply the load on a service that is already struggling, which is how one slow dependency turns into an outage for every service that calls it.
What should a caller get when the breaker rejects a call?
A fast, explicit failure it can handle. In this challenge that is the rejected outcome, and the dependency is never called. In a real service the caller might get a cached response, a default value, a degraded version of the feature, or a clear error asking the client to retry later. The point is that the answer comes back at once instead of after a timeout, so threads and connections are not tied up waiting on a dependency that cannot answer, and the dependency gets room to recover.
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- Consistent Hashing RingHow distributed caches decide which node owns a key — with tiny churn when nodes join or leave. Build a hash ring with virtual nodes and route keys clockwise. A provided hash keeps Python and TypeScript in sync. Hidden tests.Read →
- Raft Leader Election (Majority)The heartbeat of the Raft consensus algorithm: a candidate becomes leader only by winning a strict majority of the cluster — the rule that guarantees at most one leader per term. Decide an election round. Solve it in Python or TypeScript, with hidden tests.Read →
- Vector Clock MergeVector clocks let processes with no shared time agree on which event caused which. The key operation: on receive, take the element-wise max of the two clocks, then tick your own entry. Solve it in Python or TypeScript, with hidden tests.Read →
- CRDT: Grow-Only CounterA counter many replicas increment independently, with no coordination, that always converges to the same total after syncing — a G-Counter, the simplest CRDT. Merge per-replica payloads by element-wise max. Solve it in Python or TypeScript, with hidden tests.Read →
- BPE Merge StepOne step of how every tokenizer vocabulary is built: count adjacent symbol pairs across the corpus, pick the most frequent (ties break lexicographically), and merge it everywhere, left to right, without overlaps. Run it a few thousand times and you have byte-pair encoding. Solve it in Python or TypeScript.Read →
- MMR RerankingTop-k by similarity returns five copies of the same paragraph. Implement Maximal Marginal Relevance: greedily pick results that are relevant to the query AND different from what you already picked — λ·relevance − (1−λ)·redundancy, with cosine similarity. Solve it in Python or TypeScript.Read →