Reliability
How @adonis-agora/durable keeps long-running work correct in the face of transient failures, crashes and overload — step retries, saga compensation, durable flow-control queues and the dead-letter queue.
A durable workflow's whole reason to exist is that the world it talks to is unreliable: APIs time out, workers crash mid-step, downstream systems rate-limit you, and the occasional run is a poison pill that takes the process down with it. The engine treats each of those as a first-class concern rather than something you hand-roll on top.
A worker crashing mid-run is handled automatically by self-healing recovery: while a run executes its worker renews a recovery lease, and a crashed worker stops renewing, so its lease expires. Recovery (engine.recoverIncomplete()) runs both on boot and periodically — the durable:work loop calls it every tick — so an orphaned running run is reclaimed by another instance within ~leaseMs, not only on the next deploy. See Durability & replay.
The building blocks compose: a remote step can retry with backoff, register a compensation to undo itself, be admitted through a rate-limited queue, and — if recovery can never make it past a crash — land in the dead-letter state for a handler to deal with.
The four primitives
Retries & backoff
Local-step retries with fixed/exponential backoff and jitter, FatalError to opt out, and the durable remote-step retry path.
Sagas & compensation
Undo the side effects of a partially-completed run with per-step compensate callbacks that run in reverse.
Flow control
Durable queues that cap concurrency and enforce rate limits — per-instance, or distributed with Redis admission.
Dead-letter queue
Cap crash-recovery with maxRecoveryAttempts so a poison pill moves to the terminal dead status instead of crash-looping.
Retention & archival
Hard-delete terminal runs past a per-status age, and archive each one first with engine.onEvict — a throwing hook skips the delete.
Failure modes & recovery
An operator's symptom → diagnosis → fix map: a stuck suspended run, a lost remote dispatch, the stalled-run pager, queue-transport gaps, and the namespace mistake.
At a glance
- Retries — both
ctx.step(dispatched) andctx.localStep(in-process) retry on failure. A local step retries the in-process function; a dispatched step has a durable retry path (a failed dispatch re-dispatches by suspending the run on a persistedwakeAt, so retries survive a crash) plus atimeoutMsheartbeat-liveness path for presumed-dead workers. - Sagas — attach a
compensateclosure to actx.localStep, or acompensatestep ref to a dispatchedctx.step; the engine runs registered undos in reverse order when the run fails.compensationRetriesretries a transient undo, and you can trigger the saga deliberately withengine.cancel(runId, { compensate: true }). - Flow control — register a queue with
engine.registerQueueand reference it fromctx.step(step, input, { queue }). A call that can't be admitted re-suspends with a retry time. For a global cross-process cap, use the@adonis-agora/durable/admission-redisbackend. - Dead-lettering —
maxRecoveryAttemptscaps how many times crash-recovery retries a run before moving it to the terminaldeadstatus, where it stays inspectable and retriable.engine.onDeadroutes the dead run to a handler. - Retention — the
retentionconfig hard-deletes terminal runs past a per-status age, swept by the worker tick;engine.onEvictarchives each run (with its checkpoints) before deletion, and a throwing hook skips the delete. - The stalled-run pager —
engine.onStalled+stalledAfterturn the stranded signature into a page instead of a query you remember to run: once per stranded episode, a listener receives the run and why it looks stuck.
Event-triggered workflows
Start a workflow when an external event fires — an AdonisJS emitter event (`@OnEvent`) or a `@adonis-agora/diagnostics` channel (`@OnDiagnostic`) — with the event payload as the run input.
Retries & backoff
In-process localStep retries with fixed/exponential backoff and jitter, FatalError to opt out, the durable dispatched-step retry path (re-dispatch on a persisted wakeAt), retryable:false worker verdicts, and the in-memory timeoutMs + heartbeat liveness path.