Skip to content

The verdict model ​

The verdict model ​

One settled call, read differently by each policy. This table is the design:

OutcomeVerdictBreakerRetry
200 fastsuccesshealthydone
200 in 9 ssuccesscounts toward slow-ratedone
404, 422answeredhealthynever
429, 503overloadhealthyafter Retry-After
500, ECONNRESET, unlabelledtransientfailureyes
our deadline elapsedtimeoutfailureonce
we refused itrejectedignoredno

That last row matters more than it looks. Our own shedding must never be recorded as evidence about the upstream — without it, an open breaker observes its own rejections and can never close.

Four ways to be refused ​

Every rejection arrives as RejectedError.reason and on the onRejection observer hook, so "why was I refused?" always has an answer.

Refused byreasonMeans
breakercircuit-openthe upstream looks wholly down
limiterlimiter-fulltoo many in flight for current latency
throttlerthrottledtoo many recent attempts were not accepted
bulkheadbulkhead-fulla hard concurrency cap you configured
rate limiterrate-limiteda fixed rate you configured
budgetbudget-exceededthe retry was refused; the first attempt was not
priorityshed-by-prioritylower-criticality work, shed under load
fairnessunfair-sharethis tenant is furthest above its share

The rule that follows from it ​

A policy may never learn anything from a call it did not make.

When resilix refuses a call, nothing was learned about the upstream — it was never contacted. The rejected verdict carries exactly that, and it has two halves that pull in opposite directions: as evidence it is ignored everywhere, and as bookkeeping it is always delivered, so a policy that reserved a slot still releases it.

This has been violated five times during development, always with the same shape: the two halves ran at different points in the call lifecycle and a rejection slipped into the gap. It is now enforced mechanically rather than remembered — see ADR-007.

MIT licensed. Zero runtime dependencies.