Skip to content
sluice

Concepts

Every effect sluice tracks is in exactly one of four states: in_flight, succeeded, failed, or indeterminate. Three of those are final. The fourth — in_flight — is where all the interesting work happens, and where it always starts.

sluice's exactly-once state machine: in_flight to succeeded, failed, or indeterminate A directed diagram. One box, in_flight, has three outgoing arrows, drawn as a single stem that forks into three branches. The first branch leads to succeeded: the effect ran and its result was durably recorded, so every future caller replays that result instead of running the effect again. The second branch leads to failed: the effect provably did not happen, so it is safe to know the outcome and act on it. The third branch, drawn in the signal colour because it is the only one that matters for this argument, leads to indeterminate: sluice could not determine whether the effect happened, for example a timeout after the call was sent, or a crash after the effect ran but before its result was persisted. Unlike the other two branches, indeterminate has no automatic path back to in_flight. By default sluice fails closed: the effect is parked as indeterminate and never silently retried. Leaving that state requires an explicit reclaim call, where the caller has declared the downstream operation safe to repeat, or a human decision recorded through an approval gate. Retries happen inside in_flight, under a single lease, before any terminal state is reached — they never turn a failed or indeterminate outcome into a fresh attempt. in_flightretries happen here,under one lease succeededresult persisted;replayed to callers failedprovably did nothappen indeterminatewe do not know fails closed — never auto-retried in_flight is the only non-terminal state here; the other three are final once reached. Leaving indeterminate needs an explicit reclaim or a gate decision — never a retry.

Why four states, not three

Most retry logic treats an error as one thing: it failed, so retry it. sluice splits "it failed" into two states that look similar from the caller's side and are opposite in what they permit:

  • failed means the effect provably did not happen. The store has a terminal write, sluice classified the error, and there is nothing to be unsure about. Replaying returns the recorded typed error every time.
  • indeterminate means sluice does not know. A caller-side timeout after the call was already sent. A crash after the effect ran but before its result was persisted. A store write that itself failed right after a claim was granted. In every one of these, the side effect may have already happened — sluice cannot tell, and it says so instead of guessing.

Conflating the two is the exact bug sluice exists to close. A naive retry loop treats "I got an error" as one bucket and retries all of it, including the fraction that already landed. That is how a charge_card call becomes two charges: the first attempt succeeded downstream but the caller never saw the response, so it retried an operation that had already run.

The one edge that matters

Look at the diagram again: two of the three outgoing arrows from in_flight are ordinary outcomes. The third — indeterminate — is drawn in the signal colour because it behaves differently from the other two in one specific way. Both succeeded and failed are outcomes a caller can act on immediately and completely: replay the result, or know the effect never ran. indeterminate is not actionable the same way, and critically, sluice never automatically retries out of it. By default (onIndeterminate: 'fail'), the effect is parked as indeterminate and stays there until something explicit moves it:

  • reclaim — the caller declares the downstream operation safe to repeat (idempotent on its own, or acceptable to duplicate) and sluice re-executes it under a fresh claim.
  • gate — sluice opens a durable "did this land?" approval gate and a human decides, out of band. See Gates.

Both are opt-in. Say nothing, and an indeterminate charge stays parked forever rather than firing a second time on a guess. That is what "fails closed" means in every doc on this site.

Where retries actually happen

Retries are not a fifth state and not a transition back out of a terminal state — they happen inside in_flight, under one lease, before any terminal write occurs (full detail: Retries & breaker). A retryable classification tries again within the same claim; only when retries are exhausted, or the classifier says failed outright, does the record move to the terminal failed state. Nothing about the outer state machine changes shape because of that — in_flight still has exactly three possible exits, and indeterminate is still the one with no way back in on its own.

Next

  • Failure modes — all twelve fault scenarios, each naming which of the four states it lands in and why.
  • Idempotency keys — the key doctrine that makes the replay half of succeeded and failed correct in the first place.
  • Gates — the human-in-the-loop way out of indeterminate.