Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

CODING CHALLENGE · N°23

Circuit Breaker

Medium SystemsReliabilityDistributed Systems

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.

SOLVE IT YOURSELF

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.

YOUR TASK

Implement circuit_breaker(threshold, cooldown, calls) → list of "ok" / "fail" / "rejected", replaying the closed → open → half-open state machine.

CPython · WebAssembly
Examples and constraints 4 examples
EXAMPLE 1
Input threshold = 2, cooldown = 10, calls = [[0,false],[1,false],[2,true],[11,true],[12,true]]
Output ["fail","fail","rejected","ok","ok"]
trips at t=1, rejects during cooldown, probe at t=11 heals it
EXAMPLE 2
Input threshold = 1, cooldown = 10, calls = [[0,false],[10,false],[15,true],[20,true]]
Output ["fail","fail","rejected","ok"]
the probe at t=10 fails, so the breaker re-opens from t=10 and rejects t=15; the next probe, at t=20, heals it
EXAMPLE 3
Input threshold = 2, cooldown = 10, calls = [[0,false],[1,true],[2,false],[3,true]]
Output ["fail","ok","fail","ok"]
a success resets the CONSECUTIVE count, so two failures never line up
EXAMPLE 4
Input threshold = 1, cooldown = 10, calls = [[0,false],[9.999,true],[10,true]]
Output ["fail","rejected","ok"]
edge case: 9.999 is still inside the cooldown; t=10 is exactly opened_at + cooldown, so it is the probe
CONSTRAINTS
  • 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
HINTS — 4 IDEAS
  1. Track three things: state (closed/open), consecutive failure count, and when the breaker opened.
  2. Open + time < opened_at + cooldown → "rejected", and nothing else changes.
  3. 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.
  4. 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.

stepcall (t, succeeds)state beforeoutcomefailsopened_atstate after
1[0, false]closed"fail"10closed
2[1, false]closed"fail"2, trips1open
3[2, true]open, 2 < 11"rejected"21open
4[11, true]open, 11 ≥ 11: probe"ok"01closed
5[12, true]closed"ok"01closed

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 + cooldown is 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 strict t < 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 touches fails or opened_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 empty calls list 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 fails on a failed probe but leaving opened_at at 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 on opened_at + cooldown instead 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.

Finished this one? 0 / 76 Challenges done
NextConsistent Hashing Ring Next on the “Systems, limits & distribution” ladder · rung 6 of 9

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More Challenges