Skip to content

Retry Policies

A retry policy defines how DagNats handles transient step failures – how many times to retry, how long to wait between attempts, and which backoff strategy to use.

RetryPolicy Configuration

FieldJSON KeyTypeDescription
MaxAttemptsmax_attemptsintNumber of retries after the first attempt, so 3 allows up to 4 attempts in all; 0 means no retries
StrategystrategyRetryStrategyBackoff algorithm: fixed, linear, or exponential
InitialDelayinitial_delaydurationBase delay between attempts
MaxDelaymax_delaydurationDelay cap (0 = uncapped)
Multipliermultiplierfloat64Multiplier for exponential strategy only

Backoff Strategies

DagNats supports three backoff strategies, each computing the delay before the next retry attempt (1-based):

StrategyFormulaExample (initial: 2s)
fixedinitial_delay2s, 2s, 2s, 2s
linearinitial_delay * attempt2s, 4s, 6s, 8s
exponentialinitial_delay * multiplier^(attempt-1)2s, 4s, 8s, 16s

All strategies respect MaxDelay – if the computed delay exceeds the cap, MaxDelay is used instead.

Setting Retry Policies

Per-Step

The builder has no method for a full policy, so set the Retry field on the built step. WithRetries(n) on a step is the legacy shortcut: n retries at a fixed 5-second delay.

wf := dag.NewWorkflow("llm-pipeline")
wf.Task("call-llm", "llm.chat").WithTimeout(60 * time.Second)

def, err := wf.Build()
if err != nil {
    return err
}
def.Steps[0].Retry = &dag.RetryPolicy{
    MaxAttempts:  5,
    Strategy:     dag.RetryExponential,
    InitialDelay: 2 * time.Second,
    MaxDelay:     30 * time.Second,
    Multiplier:   2.0,
}

In a JSON workflow definition the same policy is the step’s retry object, with durations as strings:

{
  "id": "call-llm",
  "task": "llm.chat",
  "timeout": "60s",
  "retry": {
    "max_attempts": 5,
    "strategy": "exponential",
    "initial_delay": "2s",
    "max_delay": "30s",
    "multiplier": 2.0
  }
}

Workflow Default

Set DefaultRetry on the definition (default_retry in JSON) to apply a policy to all steps unless overridden:

def.DefaultRetry = &dag.RetryPolicy{
    MaxAttempts:  3,
    Strategy:     dag.RetryFixed,
    InitialDelay: 5 * time.Second,
    MaxDelay:     5 * time.Second,
}

Resolution Order

The engine resolves the effective retry policy for each step using this precedence:

  1. Step-level Retry field (highest priority)
  2. Workflow-level DefaultRetry
  3. Legacy Retries field (converted to fixed-delay policy with 5s delay)
  4. No retries (if none of the above are set)

NATS Implementation

The engine owns retries. The worker acks the failed task and reports step.failed; the engine computes the delay and schedules a durable retry_backoff timer on the SLEEP_TIMERS stream (itself a NakWithDelay). When the timer fires, the engine dispatches a fresh task for the next attempt, which gets the full step timeout. There is no separate timer service or retry queue.

    sequenceDiagram
    participant Worker
    participant NATS as TASK_QUEUES
    participant Engine

    participant Timers as SLEEP_TIMERS

    Worker->>Engine: step.failed (retriable)
    Engine->>Engine: calculate delay (attempt 2)
    Engine->>Timers: schedule retry_backoff (4s)
    Note over Timers: waits 4 seconds
    Timers->>Engine: timer fires
    Engine->>NATS: dispatch attempt 2
    NATS->>Worker: deliver task
  

Non-Retryable Errors

Workers can signal that a failure should not be retried by calling FailPermanent():

w.Handle("validate", func(ctx worker.TaskContext) error {
    input := ctx.Input()
    if !isValid(input) {
        return ctx.FailPermanent(fmt.Errorf("invalid input: %s", input))
    }
    return ctx.Complete([]byte(`{"valid":true}`))
})

FailPermanent() sets the failure type to non_retriable, causing the engine to skip all remaining retries and immediately proceed to on-failure handling or workflow failure.

See Error Handling for the full FailureType taxonomy.

Retrying transient LLM API failures. LLM providers frequently return 429 (rate limit) and 503 (overloaded) errors. Use exponential backoff with a generous max_delay (30-60s) and 5+ max attempts to ride out transient API unavailability without manual intervention.

Related Pages