Circuit Breaker Pattern: Failing Fast to Stay Resilient
February 15, 2026 · 7 min · 1459 words · Rob Washington
Table of Contents
When a downstream service dies, the worst thing you can do is keep hammering it with requests. Each request ties up a connection, burns through your timeout budget, and cascades the failure upstream. Circuit breakers solve this by detecting failures early and failing fast.
classCircuitBreaker{constructor(options={}){this.failureThreshold=options.failureThreshold||5;this.resetTimeout=options.resetTimeout||30000;this.halfOpenRequests=options.halfOpenRequests||3;this.state='CLOSED';this.failures=0;this.successes=0;this.lastFailureTime=null;this.halfOpenAttempts=0;}asynccall(fn){if(this.state==='OPEN'){if(Date.now()-this.lastFailureTime>=this.resetTimeout){this.state='HALF_OPEN';this.halfOpenAttempts=0;}else{thrownewCircuitOpenError('Circuit breaker is open');}}try{constresult=awaitfn();this.onSuccess();returnresult;}catch(error){this.onFailure();throwerror;}}onSuccess(){if(this.state==='HALF_OPEN'){this.successes++;if(this.successes>=this.halfOpenRequests){this.reset();}}else{this.failures=0;}}onFailure(){this.failures++;this.lastFailureTime=Date.now();if(this.state==='HALF_OPEN'){this.state='OPEN';this.successes=0;}elseif(this.failures>=this.failureThreshold){this.state='OPEN';}}reset(){this.state='CLOSED';this.failures=0;this.successes=0;this.halfOpenAttempts=0;}}classCircuitOpenErrorextendsError{constructor(message){super(message);this.name='CircuitOpenError';}}
constpaymentCircuit=newCircuitBreaker({failureThreshold:5,resetTimeout:30000,});asyncfunctionprocessPayment(order){try{returnawaitpaymentCircuit.call(async()=>{returnawaitpaymentService.charge(order);});}catch(error){if(errorinstanceofCircuitOpenError){// Fast failure - payment service is known to be down
return{status:'pending',message:'Payment service temporarily unavailable'};}throwerror;}}
classSlidingWindowCircuitBreaker{constructor(options={}){this.windowSize=options.windowSize||10;this.failureRateThreshold=options.failureRateThreshold||0.5;this.resetTimeout=options.resetTimeout||30000;this.results=[];// Ring buffer of recent results
this.state='CLOSED';this.lastFailureTime=null;}recordResult(success){this.results.push({success,timestamp:Date.now()});// Keep only recent results
if(this.results.length>this.windowSize){this.results.shift();}// Only evaluate when we have enough data
if(this.results.length>=this.windowSize){constfailures=this.results.filter(r=>!r.success).length;constfailureRate=failures/this.results.length;if(failureRate>=this.failureRateThreshold){this.state='OPEN';this.lastFailureTime=Date.now();}}}getStats(){constfailures=this.results.filter(r=>!r.success).length;return{state:this.state,totalCalls:this.results.length,failures,failureRate:this.results.length>0?failures/this.results.length:0,};}}
asyncfunctioncallWithResilience(fn,options={}){constcircuit=options.circuit;constmaxRetries=options.maxRetries||3;constbackoff=options.backoff||(attempt=>Math.pow(2,attempt)*100);returncircuit.call(async()=>{letlastError;for(letattempt=0;attempt<maxRetries;attempt++){try{returnawaitfn();}catch(error){lastError=error;// Don't retry on circuit open
if(errorinstanceofCircuitOpenError)throwerror;// Don't retry on client errors (4xx)
if(error.statusCode>=400&&error.statusCode<500)throwerror;if(attempt<maxRetries-1){awaitsleep(backoff(attempt));}}}throwlastError;});}