Home / AWS

AWSHow-toAdvanced

Step Functions error handling done right: Retry, Catch, idempotency and Distributed Map at scale

Design Step Functions workflows that survive throttling and partial failures. Retry with backoff and jitter, catch and compensate, make tasks idempotent, and run large fan-outs with Distributed Map tolerated failure thresholds and redrive.

Deepak Bhardwaj4 min read

Applies toAWS Step Functions Standard and Express workflows, Amazon States Language, Lambda, Distributed Map

Step Functions makes it easy to draw a workflow and hard to notice it has no plan for failure until production. This post is the set of patterns I put into every state machine.

Retry: be specific, back off, add jitter

"ChargeCard": {
  "Type": "Task",
  "Resource": "arn:aws:states:::lambda:invoke",
  "Parameters": { "FunctionName": "charge-card", "Payload.$": "$" },
  "Retry": [
    {
      "ErrorEquals": ["Lambda.TooManyRequestsException", "Lambda.ServiceException", "Lambda.SdkClientException"],
      "IntervalSeconds": 2, "BackoffRate": 2.0, "MaxAttempts": 6,
      "MaxDelaySeconds": 60, "JitterStrategy": "FULL"
    },
    {
      "ErrorEquals": ["PaymentGatewayTimeout"],
      "IntervalSeconds": 5, "BackoffRate": 2.0, "MaxAttempts": 3
    }
  ],
  "Catch": [
    { "ErrorEquals": ["CardDeclined"], "ResultPath": "$.error", "Next": "NotifyDeclined" },
    { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "CompensateOrder" }
  ],
  "Next": "ShipOrder"
}

Rules:

  • Retriers are evaluated in order; put specific errors first.
  • Never retry business errors (CardDeclined). Throw them as named errors from your code and catch them.
  • States.ALL in a Retry retries everything, including bugs. Use it only in Catch.
  • JitterStrategy: FULL stops many executions retrying in lockstep after a shared outage.

Tip

ResultPath: "$.error" keeps the original input and adds the error object. Without it, Catch replaces your state with the error and the next state loses the order data.

Timeouts and heartbeats

A task with no timeout can wait for a year (the Standard workflow maximum). Set both:

"TimeoutSeconds": 300,
"HeartbeatSeconds": 60

For callback tasks (.waitForTaskToken), the worker must call SendTaskHeartbeat; otherwise States.HeartbeatTimeout tells you it died.

Idempotency

Retries mean a task may run more than once. Make every side effect idempotent:

  • Pass a deterministic key, for example $$.Execution.Name plus step name, to the downstream API as an idempotency key.
  • Use conditional writes in DynamoDB:
"RecordPayment": {
  "Type": "Task",
  "Resource": "arn:aws:states:::dynamodb:putItem",
  "Parameters": {
    "TableName": "payments",
    "Item": { "pk": { "S.$": "$.orderId" }, "status": { "S": "CHARGED" } },
    "ConditionExpression": "attribute_not_exists(pk)"
  },
  "Catch": [{ "ErrorEquals": ["DynamoDB.ConditionalCheckFailedException"], "Next": "AlreadyRecorded" }],
  "Next": "ShipOrder"
}
  • Start executions with a deterministic --name so a duplicate start fails instead of running twice (Standard workflows reject a reused name for 90 days).
aws stepfunctions start-execution --state-machine-arn $SM --name order-$ORDER_ID --input file://order.json

Compensation (saga)

When step 3 fails after steps 1 and 2 succeeded, Catch to a compensation branch that undoes them in reverse: refund the charge, release the stock. Each compensation step needs its own Retry; a failed refund is worse than a failed charge.

Distributed Map for large fan-out

Distributed Map runs child executions over items in S3 (CSV, JSON, object listing) with high concurrency.

"ProcessFiles": {
  "Type": "Map",
  "ItemReader": {
    "Resource": "arn:aws:states:::s3:listObjectsV2",
    "Parameters": { "Bucket": "acme-inbox", "Prefix": "2026/10/" }
  },
  "ItemProcessor": {
    "ProcessorConfig": { "Mode": "DISTRIBUTED", "ExecutionType": "EXPRESS" },
    "StartAt": "Transform",
    "States": { "Transform": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": { "FunctionName": "transform", "Payload.$": "$" }, "End": true } }
  },
  "MaxConcurrency": 200,
  "ToleratedFailurePercentage": 2,
  "ItemBatcher": { "MaxItemsPerBatch": 50 },
  "ResultWriter": { "Resource": "arn:aws:states:::s3:putObject", "Parameters": { "Bucket": "acme-results", "Prefix": "runs/" } },
  "End": true
}
  • MaxConcurrency protects downstream services and Lambda concurrency. Size it from what the slowest dependency can handle.
  • ToleratedFailurePercentage / Count lets the run finish when a few items are bad; failed items are listed in the ResultWriter output.
  • ItemBatcher cuts per-item overhead and cost.

Redrive instead of restart

Standard workflows can be redriven from the failed state, keeping completed work:

aws stepfunctions redrive-execution --execution-arn $EXEC_ARN

Distributed Map runs can redrive only the failed child executions. Fix the bug or the dependency first; redrive does not change the definition.

Observe

aws stepfunctions list-executions --state-machine-arn $SM --status-filter FAILED --max-results 20
aws stepfunctions get-execution-history --execution-arn $EXEC_ARN --reverse-order --max-results 5

CloudWatch metrics ExecutionsFailed, ExecutionsTimedOut and ExecutionThrottled deserve alarms. Express workflows need CloudWatch Logs enabled to see anything about failures.

Prevent it next time

  • Lint definitions in CI (aws stepfunctions validate-state-machine-definition).
  • Every Task has a timeout, a specific Retry and a Catch.
  • Chaos test: make a dependency return throttling errors in staging and watch the workflow recover.