Skip to content
ExponaBLOG
AWS FOR DEVELOPERS · DAY 050 / 100

Orchestration with Step Functions: a practical AWS guide

Workflows with retries, waits and branches.

Expona Admin6 min read

By Day 50, you've built Lambda functions, connected them to APIs, and orchestrated basic workflows. But what happens when you need to retry a failed payment, wait for human approval, or branch logic based on conditions—without managing state yourself? That's where Orchestration with Step Functions comes in. AWS Step Functions lets you define complex workflows as state machines, handling retries, waits, and branching automatically so you focus on business logic, not glue code.

State Machines — Define Steps Visually or in JSON

A state machine is a formal model where your workflow moves through discrete states. Each state performs an action (invoke Lambda, call an API, run a database query) and transitions to the next based on output or conditions. Step Functions lets you define these using the AWS Management Console's visual editor or Amazon States Language (JSON).

I prefer starting with JSON because it's version-controllable and portable. Here's a simple order processing workflow:

bash
{
  "Comment": "Order processing workflow",
  "StartAt": "ValidateOrder",
  "States": {
    "ValidateOrder": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123456789:function:validate-order",
      "Next": "ProcessPayment"
    },
    "ProcessPayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123456789:function:process-payment",
      "Next": "NotifyCustomer"
    },
    "NotifyCustomer": {
      "Type": "Task",
      "Resource": "arn:aws:sns:us-east-1:123456789:OrderTopic",
      "End": true
    }
  }
}

This defines three sequential tasks. When you execute this state machine, Step Functions passes the output of one task as input to the next. The visual editor in the console renders this as a flowchart, making it easy to review with non-technical stakeholders.

State types include Task (invoke services), Choice (branch on conditions), Wait (pause execution), Parallel (run steps concurrently), and Catch (error handling). For a production order system, you might add a Choice state to route high-value orders to manual approval:

bash
{
  "ValidateOrder": {
    "Type": "Task",
    "Resource": "arn:aws:lambda:us-east-1:123456789:function:validate-order",
    "Next": "CheckOrderValue"
  },
  "CheckOrderValue": {
    "Type": "Choice",
    "Choices": [
      {
        "Variable": "$.totalAmount",
        "NumericGreaterThan": 5000,
        "Next": "ManualApproval"
      }
    ],
    "Default": "ProcessPayment"
  },
  "ManualApproval": {
    "Type": "Task",
    "Resource": "arn:aws:states:::sqs:sendMessage.waitForTaskToken",
    "Parameters": {
      "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789/approvals",
      "MessageBody": {
        "orderId.$": "$.orderId",
        "taskToken.$": "$$.Task.Token"
      }
    },
    "Next": "ProcessPayment"
  }
}

The waitForTaskToken pattern pauses execution until an external system (like a human or another service) sends a callback. This is powerful for approval workflows without requiring the state machine to poll or wait in an active Lambda.

Built-in Retries — Retry with Backoff Per Step

Network calls fail. Payment gateways timeout. Lambda occasionally throttles. Rather than building retry logic in your Lambda functions, define retries at the state level—Step Functions handles it for you.

Add a Retry block to any Task state:

bash
{
  "ProcessPayment": {
    "Type": "Task",
    "Resource": "arn:aws:lambda:us-east-1:123456789:function:process-payment",
    "Retry": [
      {
        "ErrorEquals": ["States.TaskFailed"],
        "IntervalSeconds": 2,
        "MaxAttempts": 3,
        "BackoffRate": 2.0
      }
    ],
    "Catch": [
      {
        "ErrorEquals": ["States.ALL"],
        "Next": "PaymentFailed"
      }
    ],
    "Next": "NotifyCustomer"
  },
  "PaymentFailed": {
    "Type": "Fail",
    "Error": "PaymentError",
    "Cause": "Payment processing failed after retries"
  }
}

This configuration retries the Lambda up to 3 times if it fails, starting with a 2-second delay, doubling each time (2s, 4s, 8s). The BackoffRate multiplies the IntervalSeconds exponentially—exponential backoff prevents overwhelming a struggling service. After all retries exhaust, the Catch block routes to PaymentFailed.

You can specify multiple Retry blocks with different error patterns. For example, retry on timeout but fail immediately on validation errors:

bash
"Retry": [
  {
    "ErrorEquals": ["States.TaskFailed"],
    "IntervalSeconds": 1,
    "MaxAttempts": 5,
    "BackoffRate": 2.0
  },
  {
    "ErrorEquals": ["ValidationError"],
    "IntervalSeconds": 0,
    "MaxAttempts": 0
  }
]

In real systems, I've used this to handle flaky third-party APIs. Payment gateways occasionally return 503 errors; with exponential backoff, retrying 3–5 times usually succeeds without any code changes to the Lambda itself.

Long Waits — Wait Hours or Days Without Paying for Idle Compute

Imagine an order workflow: validate, charge, then wait 24 hours before shipping (maybe for fraud checks or regulatory holds). Running a Lambda for 24 hours would cost hundreds of dollars per order. Step Functions' Wait state pauses execution without consuming resources.

bash
{
  "ProcessPayment": {
    "Type": "Task",
    "Resource": "arn:aws:lambda:us-east-1:123456789:function:process-payment",
    "Next": "WaitForFraudCheck"
  },
  "WaitForFraudCheck": {
    "Type": "Wait",
    "Seconds": 86400,
    "Next": "ShipOrder"
  },
  "ShipOrder": {
    "Type": "Task",
    "Resource": "arn:aws:lambda:us-east-1:123456789:function:ship-order",
    "End": true
  }
}

After ProcessPayment completes, the state machine sleeps for 86,400 seconds (24 hours) without any active resources. You pay nothing during the wait. After the delay, execution resumes and calls ShipOrder. Step Functions maintains the execution state internally—you can query it anytime to see how many orders are waiting.

You can also use dynamic waits based on execution data:

bash
{
  "WaitForApproval": {
    "Type": "Wait",
    "SecondsPath": "$.approvalTimeoutSeconds",
    "Next": "CheckApprovalStatus"
  }
}

If the input JSON contains approvalTimeoutSeconds, that value becomes the wait duration. This lets you build flexible workflows where wait times vary by order priority or customer tier.

I've used this pattern for travel booking workflows: collect confirmation, wait up to 48 hours for payment, then release or rebook. Without Step Functions, you'd need a cron job, database, and Lambda running constantly. Step Functions makes it trivial and cost-effective.

Express vs Standard — Express for High-Volume Short Workflows

Step Functions offers two workflow types: Standard and Express.

Standard workflows are the default. They run for up to 1 year, support all state types, and charge per state transition. Ideal for long-running, low-frequency processes like approval chains or order processing.

Express workflows run for up to 5 minutes and are optimized for high-volume, short-lived tasks (think 1,000+ concurrent executions). They charge per GB-second of execution. Use Express for event-driven scenarios like API request handlers or IoT data pipelines.

Feature Standard Express
Max duration 1 year 5 minutes
Max executions/second 2,000 (account limit) 100,000+
Pricing model Per state transition Per GB-second
Supported states All (Task, Choice, Wait, Parallel, Map, etc.) All
Best for Order processing, approvals, long waits Real-time APIs, high-throughput pipelines

At my ride-hailing platform, we use Express workflows to coordinate driver-passenger matching: receive request, query nearby drivers, evaluate preferences, assign, and notify—all in under 30 seconds. The high throughput justifies Express. For internal order processing, Standard is better because workflows pause for hours and rarely complete in 5 minutes.

Choose based on duration and volume. If you're unsure, start Standard; it's more flexible and handles longer workflows gracefully.

Common Mistakes

  • Over-retrying transient errors: Setting MaxAttempts too high can waste time and cost. A payment that fails due to insufficient funds won't succeed with retries. Use Retry only for transient issues (timeouts, throttles, temporary unavailability).
  • Ignoring execution history limits: Step Functions stores execution history for 90 days. For high-volume workflows, this can become expensive. Use Express workflows for short-lived, high-throughput tasks to reduce storage overhead.
  • Not catching all error paths: A missing Catch block causes the entire execution to fail. Always add a Catch clause for Task states that interact with external services, or your workflow will silently fail with no fallback.
  • Choosing Express for long-running workflows: Express has a 5-minute timeout. If your workflow waits for hours or approvals, you'll hit the limit. Always choose Standard for workflows with Wait states or approval loops.
  • Treating Step Functions as a scheduler: Step Functions isn't a cron job. Use EventBridge to trigger workflows on a schedule, then feed that into Step Functions for execution.

Key Takeaways

  • Step Functions eliminates boilerplate orchestration logic; define workflows as state machines in JSON or the visual editor.
  • Retry and backoff strategies are built in at the state level—no need to implement them in Lambda code.
  • Wait states pause execution without active compute costs, enabling cost-effective long-running workflows.
  • Standard workflows suit long-running, low-frequency processes; Express workflows handle high-volume, short-lived tasks.
  • Always pair external service calls with Retry and Catch blocks to handle transient failures and error paths gracefully.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles