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.