Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Retries and timeouts

Retries and timeouts bound how long a step may take and how often the runtime tries it again. A retry block sets the policy for one step, timeout and queue_timeout limit each attempt, and the workflow timeout limits the whole execution. This page lists every setting with its default, and describes exactly when the runtime retries.

Retry policy

step charge {
    worker "payments.charge"
    with { order_id: input.order_id, amount: input.amount, currency: input.currency }
    retry {
        attempts: 5
        backoff: exponential
        initial_delay: 500ms
        max_delay: 10s
        non_retryable: ["CARD_DECLINED", "INSUFFICIENT_FUNDS"]
    }
}

retry is followed directly by { (no :). Inside, each setting is name: value, separated by commas, semicolons or newlines.

SettingTypeDefaultMeaning
attemptsinteger literal, 1 to 10001Total number of attempts, including the first. 1 means no retries.
backofffixed or exponentialfixedHow the delay between attempts grows.
initial_delayduration literal1sThe delay before the first retry.
max_delayduration literal60s, or initial_delay if that is longerThe upper bound for exponential delays. Must be at least initial_delay.
non_retryablelist of string literals[]Error codes that fail the step immediately, without retrying.

A step without a retry block makes one attempt. A retry block that omits a setting uses that setting’s default, so retry { attempts: 3 } retries twice with a fixed 1 second delay.

Every value must be a literal: expressions such as attempts: input.n are rejected. The compiler reports every problem with a retry block as E0410:

ProblemMessage
attempts outside 1 to 1000`attempts` must be between 1 and 1000
non-literal attempts`attempts` must be an integer literal
unknown backoffunknown backoff strategy `linear`
non-duration delay`initial_delay` must be a duration literal such as `5s`
max_delay below initial_delay`max_delay` must be at least `initial_delay`
non-string error codeerror codes must be string literals
unknown settingunknown retry setting `backof` (with a suggestion)
repeated settingduplicate retry setting `attempts`

Backoff

The delay before attempt n (attempt 2 is the first retry) is:

BackoffDelay before attempt n
fixedinitial_delay
exponentialmin(initial_delay × 2^(n−2), max_delay)

For attempts: 6, backoff: exponential, initial_delay: 500ms, max_delay: 4s:

Attempt123456
Delay before itnone500 ms1 s2 s4 s4 s

The delay is a durable timer: a retry that is waiting survives a runtime restart, and the attempt starts when the timer fires.

When the runtime retries

After an attempt fails, the runtime schedules another attempt only if all of these hold:

  1. the error is retryable,
  2. the error code is not listed in non_retryable (codes are compared exactly, including case),
  3. attempts remain (attempt < attempts),
  4. the workflow is not being aborted.

Otherwise the step fails permanently. If the policy allowed more than one attempt and they were all used, the error message ends with (gave up after N attempts).

Whether an error is retryable is decided by whoever reports it. Workers report errors with a code and a retryable flag (see How workers work). Errors that the runtime generates have a fixed classification:

CodeRetryableCause
TIMEOUTyesThe attempt exceeded the step timeout.
HTTP_ERRORdependsNetwork failures and 5xx/429 responses are retryable; invalid requests are not. See HTTP adapter.
QUEUE_TIMEOUTnoNo worker picked the task up within queue_timeout.
INPUT_CONTRACT_VIOLATIONnoThe evaluated input does not match the contract.
OUTPUT_CONTRACT_VIOLATIONnoThe worker’s output does not match the contract.
EXPRESSION_ERRORnoEvaluating with, when, for or idempotency_key failed.
ADAPTER_DISABLEDnoA built-in task ran on a server without the adapter enabled.

Every retry reuses the input and idempotency key that were evaluated when the step was first scheduled. Redelivery after a lost worker is not a retry and does not use up an attempt; see Delivery guarantees.

Step timeout

timeout limits a single attempt, measured from the moment the task is dispatched to a worker until its result arrives:

step analyze {
    worker "risk.analyze"
    with { customer_id: input.customer_id, amount: input.amount }
    timeout 10s
    retry { attempts: 3, backoff: exponential, initial_delay: 1s }
}
PropertyValue
Defaultno limit (orbit.http.request uses 30 seconds when no timeout is set)
Applies totask steps, workflow steps and compensate blocks
On expirythe attempt fails with TIMEOUT, which is retryable
Zerorejected (E0410)

Timeouts are cooperative. The worker is told to stop, through the cancellation list it receives with heartbeat responses, but the runtime cannot force an external operation to stop. If the timed-out attempt later reports success while the step is still unresolved, the runtime accepts that result, because the work did happen.

On a workflow step, timeout limits how long the parent waits for the child. When it expires, the step fails with TIMEOUT (workflow steps cannot retry) and the child execution is cancelled.

On an await step, timeout limits how long the step waits for its signal, from the moment it starts waiting. When it expires, the step fails with SIGNAL_TIMEOUT, which is never retried. See Signals and queries.

Queue timeout

queue_timeout limits how long an attempt may wait for a worker to pick it up:

step charge {
    worker "payments.charge"
    with { order_id: input.order_id, amount: input.amount, currency: input.currency }
    queue_timeout 2m
}
PropertyValue
Defaultno limit: a task waits until a matching worker connects
Measured fromthe moment the attempt is scheduled (each retry starts a new wait)
Applies totask steps and compensate blocks; not workflow or sleep steps
On expirythe step fails with QUEUE_TIMEOUT, which is never retried
Redeliveriesnot limited by queue_timeout
Zerorejected (E0410)

Use queue_timeout to fail fast when no worker for a task (or for the using runtime) is running, instead of waiting until the workflow deadline.

Workflow timeout

The workflow-level timeout limits the whole execution, measured from its creation, across all steps, retries, sleeps and waits for signals. When it passes, the runtime stops scheduling work, waits for in-flight attempts, runs compensations, and ends the execution as TIMED_OUT. See Workflows.

Where each setting is allowed

SettingTask stepWorkflow stepSleep stepcompensate block
retryyesno (E0307)no (E0307)yes
timeoutyesyesno (E0307)yes
queue_timeoutyesno (E0307)no (E0307)yes

await steps accept timeout only, and compute steps accept none of the three (E0307): a compute step is evaluated by the runtime and cannot time out or be retried.

A child workflow is durable on its own. Put retries on the child’s steps.

Retries in the history

Each attempt appears in the execution history. A step with retry { attempts: 3, backoff: exponential, initial_delay: 100ms } that fails every time produces events like these (orbit logs <id>):

+   106ms   #9  StepScheduled             step=broken attempt=1
+   201ms  #10  StepDispatched            step=broken attempt=1 worker_id=orbit-runtime
+   203ms  #11  StepAttemptFailed         step=broken attempt=1 error=HTTP_ERROR request failed: io: Connection refused (os error 111)
+   205ms  #12  RetryScheduled            step=broken delay_ms=100
+   401ms  #13  StepScheduled             step=broken attempt=2
+   403ms  #14  StepDispatched            step=broken attempt=2 worker_id=orbit-runtime
+   406ms  #15  StepAttemptFailed         step=broken attempt=2 error=HTTP_ERROR request failed: io: Connection refused (os error 111)
+   407ms  #16  RetryScheduled            step=broken delay_ms=200
+   701ms  #17  StepScheduled             step=broken attempt=3
+   702ms  #18  StepDispatched            step=broken attempt=3 worker_id=orbit-runtime
+   704ms  #19  StepAttemptFailed         step=broken attempt=3 error=HTTP_ERROR request failed: io: Connection refused (os error 111)
+   705ms  #20  StepFailed                step=broken error=HTTP_ERROR request failed: io: Connection refused (os error 111) (gave up after 3 attempts)

See also