How to Set Up AWS Step Functions: 12 Steps, 90 Min [2026]

AWS Lambda gets a function running in milliseconds. But the moment that function needs to call three other services, wait for a callback, retry on failure, and branch based on a result, a single function stops being enough. That’s the gap AWS Step Functions fills, and it’s why “step functions tutorial” keeps showing up in search consoles for backend teams moving off cron jobs and hand-rolled retry logic. This guide walks through building a real state machine from scratch, deploying it with the AWS CDK, and avoiding the mistakes that turn a $5/month workflow into a $500 surprise on next month’s bill.

By the end you’ll have a working order-processing workflow that calls Lambda functions, branches on business logic, retries transient failures, and fans out across thousands of records using Distributed Map — plus the troubleshooting checklist to keep it running in production.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

What AWS Step Functions Actually Does

AWS Step Functions is a serverless orchestration service. Per AWS’s own documentation, it “makes it easy to coordinate the components of distributed applications as a series of steps in a visual workflow”. Instead of writing orchestration logic inside a Lambda function — try this, catch that, wait, retry, call the next thing — you describe the flow declaratively in a JSON-based language called Amazon States Language (ASL), and AWS runs it as a managed state machine.

The workflows themselves are called state machines. AWS’s concepts documentation is direct about this: “Step Functions is based on state machines, which are also called workflows.” Each state machine is built from individual states — Task states that do work (usually by invoking a Lambda function or calling another AWS service directly), Choice states that branch, Parallel states that fan out, Map states that iterate, and Wait states that pause.

Where this gets useful in practice: order processing pipelines, video transcoding jobs, ETL pipelines, ML training pipelines, and human-approval workflows (loan applications, content moderation) that need to pause for hours or days waiting for a callback token. AWS itself frames the use case broadly: you can “orchestrate AWS Lambda functions and other AWS resources to form complex and robust workflows” — and critically, many of those “other AWS resources” don’t need a Lambda function in between at all. Step Functions can now call more than 220 AWS services directly as of August 2026, per AWS’s own getting-started documentation, spanning DynamoDB, SNS, SQS, EventBridge, and beyond through what AWS calls optimized service integrations, with AWS documentation putting the total addressable surface at over 10,000 potential API actions as of the same August 2026 update. That reach grew in visible jumps: a January 2025 AWS What’s New release added 36 more integrated services and roughly 1,000 new API actions in one shot, an April 3, 2025 update layered on 137 new API actions just for the Backup Search integration, and a March 26, 2026 SDK integrations launch added another 28 services and 1,100-plus API actions in a single announcement.

Real-World Use Cases Worth Knowing Before You Build

Reading the AWS docs in isolation, it’s easy to think Step Functions is just “Lambda with extra steps.” In practice, teams reach for it in a handful of recurring scenarios, and knowing which one you’re solving for shapes almost every design decision that follows.

E-commerce order orchestration is the classic case this tutorial builds toward — validate, check inventory, charge, fulfill, notify — with a Catch block at every step that could plausibly fail in the real world (a card decline, a warehouse system timeout, a shipping API outage). The value isn’t the individual Lambda calls; it’s having one place that shows you exactly which of the 40,000 orders that ran last night got stuck at the payment step versus the fulfillment step.

Data pipeline orchestration is the second-most common pattern: pull raw files from S3, trigger a Glue job or a batch of Lambda transforms, validate schema, load into Redshift or a data lake, and alert on failure. Distributed Map (covered in Step 10) exists largely because of this use case — nightly ETL jobs processing millions of rows don’t fit inside a single Lambda’s 15-minute execution ceiling, and Step Functions’ fan-out model handles that without you managing a queue-and-worker system by hand.

ML training and inference pipelines lean on Step Functions to chain SageMaker training jobs, model evaluation steps, and conditional deployment logic — only promote a model to production if its evaluation metrics clear a threshold, otherwise route to a human-review Choice branch. The Wait-for-token pattern from Step 11 is exactly what teams use here when a data scientist needs to eyeball a model’s output before it ships.

Video and media processing — transcoding a single upload into six output resolutions via MediaConvert, generating thumbnails, running content-moderation checks — maps almost one-to-one onto a Parallel state, where every branch runs concurrently and the workflow only proceeds once all of them report back.

Microservices saga orchestration is the pattern most relevant to teams migrating away from tightly coupled service calls. Instead of Service A calling Service B calling Service C synchronously (and Service A having no idea what to do if C fails three hops downstream), Step Functions owns the whole saga, including the compensating transactions — cancel the reservation, refund the charge, release the inventory hold — that undo a partially completed multi-service transaction cleanly.

Prerequisites and Tool Versions

Before starting, get these in place. Version mismatches are the single biggest cause of “it worked in the tutorial but not for me” support threads.

ToolMinimum versionInstall commandWhy you need it
AWS CLIv2.22 or latercurl install per AWS docsAuth, manual state machine testing
Node.js20.x LTSnvm install 20Runs the CDK toolkit
AWS CDK2.170 or laternpm install -g aws-cdkInfrastructure-as-code deploy
Python (for Lambda code)3.12 or 3.13 runtimeN/A (Lambda-managed)Business logic in Task states
AWS account with IAM permissionsN/AN/ACreate state machines, Lambda, IAM roles
jq1.7+apt/brew install jqParsing execution output in the terminal

You’ll also want an IAM user or role with permissions for states:*, lambda:*, iam:CreateRole, iam:PassRole, and cloudformation:* if you’re deploying through CDK. Don’t use your root account. If you’re following along in a sandbox account, AdministratorAccess is fine for learning purposes — just don’t ship that IAM policy to production.

Step 1: Understand Standard vs. Express Workflows Before You Build Anything

This is the decision that determines your pricing model, your execution history, and whether Step Functions is even the right tool. Step Functions ships two workflow types, and picking wrong means a painful migration later.

AttributeStandard WorkflowsExpress Workflows
Max durationUp to 1 yearUp to 5 minutes
Execution modelExactly-onceAt-least-once (or at-most-once)
Pricing basisPer state transitionPer request + duration (GB-seconds)
Free tier4,000 state transitions/monthNot covered by the same free tier
Execution history visibilityFull visual execution history in consoleLogs go to CloudWatch Logs, not the visual history
Execution history limitAround 25,000 events per executionNo documented execution-history event limit
Best forLong-running, auditable business workflows (order fulfillment, approvals)High-volume, short-duration event processing (IoT ingestion, mobile backends)

The pricing gap is the part teams get burned by. Standard workflow pricing starts at $0.025 per 1,000 state transitions above the free tier, according to AWS’s own Distributed Map announcement blog, with the free tier still covering 4,000 state transitions per month as confirmed on the official pricing page as of August 2026. Express workflows instead bill around $1.00 per 1 million requests plus roughly $0.06 per GB-hour of compute duration, prorated to 100ms increments. If you’re running a workflow 50,000 times a day with 10 states each, Standard workflows will rack up 500,000 transitions a day — that’s real money at scale. Express workflows sidestep that entirely by charging for duration instead of transition count.

Rule of thumb: if a human might need to see the exact path an execution took through your workflow (compliance, debugging, customer support), use Standard. If you’re processing millions of short events and nobody’s going to click into a single execution’s history, use Express.

Step 2: Design the State Machine on Paper First

Before opening a code editor, sketch the workflow. For this tutorial we’re building an order-processing pipeline: validate the order, check inventory, charge the payment, and either confirm or roll back. This is deliberately close to a real e-commerce workflow because that’s the most common real-world use case for Step Functions among teams migrating off cron-based batch scripts.

The flow: ValidateOrder → CheckInventory (Choice: in stock or not) → ChargePayment (with retry on transient card-processor errors) → Choice on payment result → ConfirmOrder or RefundAndNotify. Each box in that flow becomes a state in Amazon States Language. Drawing it out first is what keeps your ASL definition from becoming an unreadable nested mess of Choice states three levels deep.

Step 3: Write the Lambda Functions

Step Functions orchestrates; it doesn’t compute. Each Task state in our workflow calls a Lambda function that does one job and returns a small, predictable payload. Keep these functions narrow — a common mistake is writing one giant Lambda that does validation, inventory, and payment all in one, which defeats the entire point of orchestration.

# validate_order.py
import json

def handler(event, context):
    order = event.get("order", {})
    errors = []

    if not order.get("customer_id"):
        errors.append("missing customer_id")
    if not order.get("items"):
        errors.append("order has no items")
    if order.get("total", 0) <= 0:
        errors.append("total must be positive")

    if errors:
        raise ValueError(f"Order validation failed: {', '.join(errors)}")

    return {
        "order_id": order["order_id"],
        "customer_id": order["customer_id"],
        "items": order["items"],
        "total": order["total"],
        "validated": True
    }

Note the pattern: raising a Python exception (or throwing in Node) is how you signal failure to Step Functions. The state machine catches that exception type and routes it through a Catch block — you don't need to manually construct error JSON. Here's the payment-charging function, which is the one most likely to hit a transient failure worth retrying:

# charge_payment.py
import json
import random

class PaymentGatewayTimeout(Exception):
    pass

class PaymentDeclined(Exception):
    pass

def handler(event, context):
    order_id = event["order_id"]
    total = event["total"]

    # Simulated gateway call — replace with real Stripe/Adyen SDK call
    if random.random() < 0.1:
        raise PaymentGatewayTimeout(f"Gateway timeout for order {order_id}")

    if total > 10000:
        raise PaymentDeclined(f"Order {order_id} exceeds fraud threshold")

    return {
        "order_id": order_id,
        "charge_id": f"ch_{order_id}_confirmed",
        "amount_charged": total,
        "payment_status": "succeeded"
    }

Deploy both as separate Lambda functions (Python 3.13 runtime, 128MB memory is plenty for logic this light). Note their ARNs — you'll need them in the state machine definition.

Step 4: Write the State Machine in Amazon States Language

ASL is JSON. Every state machine needs a StartAt field and a States object. Here's the core of our order-processing workflow with retry and catch logic built in — this is the part most tutorials skip, and it's the part that actually matters in production:

{
  "Comment": "Order processing workflow",
  "StartAt": "ValidateOrder",
  "States": {
    "ValidateOrder": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123456789012:function:validate-order",
      "Next": "ChargePayment",
      "Catch": [
        {
          "ErrorEquals": ["States.ALL"],
          "ResultPath": "$.error",
          "Next": "RejectOrder"
        }
      ]
    },
    "ChargePayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123456789012:function:charge-payment",
      "Retry": [
        {
          "ErrorEquals": ["PaymentGatewayTimeout"],
          "IntervalSeconds": 2,
          "MaxAttempts": 3,
          "BackoffRate": 2.0
        }
      ],
      "Catch": [
        {
          "ErrorEquals": ["PaymentDeclined"],
          "ResultPath": "$.error",
          "Next": "RejectOrder"
        }
      ],
      "Next": "ConfirmOrder"
    },
    "ConfirmOrder": {
      "Type": "Task",
      "Resource": "arn:aws:sns:us-east-1:123456789012:order-confirmations",
      "End": true
    },
    "RejectOrder": {
      "Type": "Task",
      "Resource": "arn:aws:sns:us-east-1:123456789012:order-rejections",
      "End": true
    }
  }
}

Two things are doing the real work here. The Retry block on ChargePayment retries only on PaymentGatewayTimeout — a transient error — up to 3 times with exponential backoff starting at 2 seconds. It will not retry a PaymentDeclined error, because retrying a declined card is pointless and will just annoy your payment processor's fraud systems. The Catch block routes both validation failures and permanent payment declines to a rejection path instead of letting the whole execution fail silently.

Notice also that ConfirmOrder and RejectOrder call SNS directly by ARN — no Lambda function in between. This is an optimized service integration, and it's one of the most underused features of Step Functions. AWS's own SAM documentation confirms this pattern is a first-class part of the service: you can orchestrate Lambda functions and other AWS resources directly. Skipping the Lambda middleman for simple service calls cuts both cost and latency.

Step 5: Create the State Machine's IAM Execution Role

Step Functions needs permission to invoke your Lambda functions and publish to SNS. Create a minimal role rather than reusing an admin role — this is the step people skip and then get bitten by during a security review.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "lambda:InvokeFunction",
      "Resource": [
        "arn:aws:lambda:us-east-1:123456789012:function:validate-order",
        "arn:aws:lambda:us-east-1:123456789012:function:charge-payment"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "sns:Publish",
      "Resource": [
        "arn:aws:sns:us-east-1:123456789012:order-confirmations",
        "arn:aws:sns:us-east-1:123456789012:order-rejections"
      ]
    }
  ]
}

Attach this as an inline policy to a role with a trust relationship allowing states.amazonaws.com to assume it. If you deploy via the CDK (next section), this role gets generated automatically with least-privilege scoping — one of the better arguments for not hand-rolling IAM policies in the console.

Step 6: Deploy With the AWS CDK Instead of the Console

The console's Workflow Studio is a genuinely good visual drag-and-drop editor for prototyping state machines and is worth opening at least once to see how ASL maps to boxes and arrows. But for anything that ships, define the state machine as code so it's versioned, reviewable, and repeatable. Here's the same workflow in CDK (TypeScript):

import * as cdk from 'aws-cdk-lib';
import * as sfn from 'aws-cdk-lib/aws-stepfunctions';
import * as tasks from 'aws-cdk-lib/aws-stepfunctions-tasks';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

export class OrderWorkflowStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const validateOrderFn = lambda.Function.fromFunctionName(
      this, 'ValidateOrderFn', 'validate-order'
    );
    const chargePaymentFn = lambda.Function.fromFunctionName(
      this, 'ChargePaymentFn', 'charge-payment'
    );

    const validateOrder = new tasks.LambdaInvoke(this, 'ValidateOrder', {
      lambdaFunction: validateOrderFn,
      outputPath: '$.Payload',
    });

    const chargePayment = new tasks.LambdaInvoke(this, 'ChargePayment', {
      lambdaFunction: chargePaymentFn,
      outputPath: '$.Payload',
    }).addRetry({
      errors: ['PaymentGatewayTimeout'],
      interval: cdk.Duration.seconds(2),
      maxAttempts: 3,
      backoffRate: 2.0,
    });

    const rejectOrder = new sfn.Pass(this, 'RejectOrder');
    const confirmOrder = new sfn.Pass(this, 'ConfirmOrder');

    chargePayment.addCatch(rejectOrder, { errors: ['PaymentDeclined'] });
    validateOrder.addCatch(rejectOrder, { errors: ['States.ALL'] });

    const definition = validateOrder.next(chargePayment).next(confirmOrder);

    new sfn.StateMachine(this, 'OrderWorkflow', {
      definitionBody: sfn.DefinitionBody.fromChainable(definition),
      stateMachineType: sfn.StateMachineType.STANDARD,
      timeout: cdk.Duration.minutes(5),
    });
  }
}

Run cdk diff before every deploy to see exactly what changes, then cdk deploy. The CDK generates the IAM role, the log group, and the state machine resource in one CloudFormation stack — which means one cdk destroy tears it all down cleanly too, no orphaned IAM roles left behind.

Step 7: Test Locally Before Deploying to AWS

AWS Step Functions Local is a downloadable Docker container that runs a functional emulation of the service on your machine, letting you test state transitions without burning real executions or waiting on network round-trips to us-east-1.

docker run -p 8083:8083 amazon/aws-stepfunctions-local

# In a second terminal, create the state machine against the local endpoint
aws stepfunctions create-state-machine \
  --endpoint-url http://localhost:8083 \
  --definition file://order-workflow.json \
  --name "OrderWorkflowLocal" \
  --role-arn "arn:aws:iam::123456789012:role/DummyRole"

# Start an execution with test input
aws stepfunctions start-execution \
  --endpoint-url http://localhost:8083 \
  --state-machine-arn "arn:aws:states:us-east-1:123456789012:stateMachine:OrderWorkflowLocal" \
  --input '{"order":{"order_id":"ord_1","customer_id":"cust_1","items":["sku_1"],"total":49.99}}'

Local testing catches ASL syntax errors and basic transition-logic bugs instantly instead of after a 30-second cold deploy. It won't catch IAM permission problems (there's no real IAM in the local emulator), so budget time for a second round of testing against a real dev-account state machine before calling anything production-ready.

Step 8: Run and Inspect Your First Execution

Once deployed for real, start an execution from the CLI and watch it in the console's visual execution history — this is one of Step Functions' best debugging features and something a plain Lambda function orchestrating its own retries simply can't give you.

aws stepfunctions start-execution \
  --state-machine-arn "arn:aws:states:us-east-1:123456789012:stateMachine:OrderWorkflow" \
  --name "test-run-$(date +%s)" \
  --input '{"order":{"order_id":"ord_42","customer_id":"cust_9","items":["sku_7"],"total":129.50}}'

# Check status
aws stepfunctions describe-execution \
  --execution-arn "arn:aws:states:us-east-1:123456789012:execution:OrderWorkflow:test-run-1755590000" \
  | jq '.status, .output'

Expected output for a successful run:

"SUCCEEDED"
{
  "order_id": "ord_42",
  "charge_id": "ch_ord_42_confirmed",
  "amount_charged": 129.5,
  "payment_status": "succeeded"
}

In the console, click into that execution and you'll see every state highlighted green (succeeded), with the exact input and output JSON captured at each transition. That per-state input/output snapshot is what makes Step Functions dramatically easier to debug in production than a single Lambda with try/except blocks scattered through 400 lines of code.

Step 9: Add JSONata for Cleaner Data Transformation

Historically, transforming data between states in ASL meant chaining InputPath, Parameters, and ResultPath fields with JSONPath — functional, but ugly and hard to read once a workflow has more than four or five states. AWS now recommends a cleaner alternative: Step Functions implements JSONata based on the 2.0.6 specification, and per AWS's own transforming-data documentation, JSONata is the recommended query language for new state machines as of the current documentation. Existing workflows built on the older JSONPath model keep working — you have to opt in explicitly per the input/output processing docs, so nothing breaks if you don't touch it.

Here's the same payment-charging state written with JSONata instead of JSONPath-style Parameters:

{
  "ChargePayment": {
    "Type": "Task",
    "Resource": "arn:aws:lambda:us-east-1:123456789012:function:charge-payment",
    "QueryLanguage": "JSONata",
    "Arguments": {
      "order_id": "{% $states.input.order_id %}",
      "total": "{% $states.input.total %}"
    },
    "Output": "{% $states.result %}",
    "Next": "ConfirmOrder"
  }
}

JSONata lets you write inline expressions — string concatenation, arithmetic, conditionals — directly in the state definition instead of bolting on a separate Lambda just to reshape a payload. For a new project starting today, default to JSONata. For an existing Standard or Express workflow already running in production on JSONPath, there's no urgency to migrate; it isn't being deprecated.

Step 10: Scale Out With Distributed Map

The regular inline Map state is fine for iterating over a few dozen or a few hundred items, but it inherits the ~25,000-event execution history ceiling of the state machine it's running in, which becomes a real constraint fast. Distributed Map was built specifically to solve that — it's designed, in AWS's words, for large-scale parallel data processing, reading items straight from an S3 bucket (JSON or CSV) and spinning up a separate child execution per item.

The scale numbers matter here: Distributed Map can launch up to 10,000 parallel child workflow executions in a single Map Run — a hard quota per AWS's service quotas documentation — and per the Distributed Map docs, if you don't explicitly cap concurrency, it defaults to running all 10,000 in parallel. Each account can have up to 1,000 open Map Runs at once, also a hard quota. On the dataset side, it will process CSV or JSON source files up to 10GB in S3 and stops after 100 million items — more than enough headroom for nearly any batch job you'll run. AWS also quietly expanded visibility into this layer on September 18, 2025, when its documentation history added new Distributed Map metrics such as Approximate Open Map Runs Count, giving you a way to see how many Map Runs are queued or open across an account without polling individual executions.

{
  "ProcessAllOrders": {
    "Type": "Map",
    "ItemProcessor": {
      "ProcessorConfig": {
        "Mode": "DISTRIBUTED",
        "ExecutionType": "STANDARD"
      },
      "StartAt": "ProcessSingleOrder",
      "States": {
        "ProcessSingleOrder": {
          "Type": "Task",
          "Resource": "arn:aws:lambda:us-east-1:123456789012:function:process-order",
          "End": true
        }
      }
    },
    "ItemReader": {
      "Resource": "arn:aws:states:::s3:getObject",
      "ReaderConfig": {
        "InputType": "CSV",
        "CSVHeaderLocation": "FIRST_ROW"
      },
      "Parameters": {
        "Bucket": "order-batch-imports",
        "Key": "daily-orders.csv"
      }
    },
    "MaxConcurrency": 500,
    "End": true
  }
}

Setting MaxConcurrency explicitly (500 in the example above, rather than leaving it unset) is the single most important tuning knob here — it stops a 100,000-row CSV from instantly slamming your downstream Lambda's concurrency limit and triggering account-wide throttling on unrelated functions.

Step 11: Handle Human-in-the-Loop Approval With Callback Tokens

Not every workflow can run to completion in seconds. Loan approvals, content moderation escalations, and manual QA gates need to pause for hours or days, and this is where Standard workflows' up-to-one-year duration limit actually earns its keep. The pattern is the .waitForTaskToken service integration: Step Functions calls a Lambda (or sends an SNS/SQS message) that hands off a task token, then the execution genuinely pauses — no polling, no wasted compute — until something outside the workflow calls SendTaskSuccess or SendTaskFailure with that token.

{
  "RequestManagerApproval": {
    "Type": "Task",
    "Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken",
    "Parameters": {
      "FunctionName": "arn:aws:lambda:us-east-1:123456789012:function:request-approval",
      "Payload": {
        "order_id.$": "$.order_id",
        "task_token.$": "$$.Task.Token"
      }
    },
    "TimeoutSeconds": 259200,
    "Next": "ConfirmOrder"
  }
}

The TimeoutSeconds field (259,200 seconds = 3 days here) matters — without it, a workflow can sit open indefinitely waiting on a token nobody ever sends, quietly racking up Standard-workflow execution time that shows up nowhere until someone audits open executions.

Step 12: Set Up CloudWatch Alarms Before You Need Them

Step Functions emits CloudWatch metrics automatically — ExecutionsFailed, ExecutionsTimedOut, ExecutionsAborted — without any extra instrumentation code, and since October 30, 2025 AWS has shipped a dedicated Step Functions metrics dashboard that surfaces account-level and state-machine-level metrics out of the box, so you no longer have to hand-build a CloudWatch dashboard just to see the shape of your failures at a glance. Wire an alarm on ExecutionsFailed with a threshold of 1 over a 5-minute period for any business-critical workflow. The most common production incident with Step Functions isn't the service failing; it's a downstream Lambda quietly failing 100% of the time for two days because nobody was watching, while the state machine dutifully logs every failed execution to a console nobody opened.

aws cloudwatch put-metric-alarm \
  --alarm-name "OrderWorkflow-ExecutionFailures" \
  --namespace "AWS/States" \
  --metric-name "ExecutionsFailed" \
  --dimensions Name=StateMachineArn,Value=arn:aws:states:us-east-1:123456789012:stateMachine:OrderWorkflow \
  --statistic Sum \
  --period 300 \
  --threshold 1 \
  --comparison-operator GreaterThanOrEqualToThreshold \
  --evaluation-periods 1 \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:oncall-alerts

5 Common Pitfalls That Break Step Functions Workflows

1. Retrying non-idempotent operations. If ChargePayment isn't idempotent and a Retry block fires after a timeout where the charge actually succeeded server-side, you double-charge the customer. Always pass an idempotency key to payment/write operations and let the downstream service dedupe, rather than trusting the retry count alone.

2. Catching States.ALL too broadly and too early. A blanket Catch on States.ALL at the very first state swallows every error type — including ones you'd want to route differently, like a permissions error versus a validation error. Scope catches to specific error names where the downstream handling actually differs.

3. Forgetting ResultPath and overwriting the whole input. By default, a Task state's output replaces the entire state input unless you set ResultPath. Downstream states that expected the original order data alongside the new payment result get a rude surprise when that data silently disappears.

4. Not setting MaxConcurrency on Map states. An unbounded Map or Distributed Map state will happily fan out to thousands of concurrent Lambda invocations, hit your account's reserved concurrency limit, and start throttling every other Lambda function in the account — including ones with nothing to do with this workflow.

5. Choosing Standard workflows for high-frequency, short-duration triggers. Running a Standard workflow that fires on every API request at 500 requests/second turns into a state-transition bill that dwarfs the Lambda cost it's orchestrating. That's an Express workflow use case, full stop.

Step Functions vs. Azure Logic Apps vs. Google Cloud Workflows

Teams running multi-cloud or evaluating a migration ask this constantly. Functionally, all three do the same core job — orchestrate calls across multiple services with retries, branching, and state — but the pricing philosophy and integration depth differ meaningfully.

PlatformPricing modelNative visual editorLong-running (hours/days) support
AWS Step FunctionsPer state transition (Standard) or per request + GB-seconds (Express)Workflow StudioYes, up to 1 year on Standard
Azure Logic AppsPer-action / per-connector executionLogic App DesignerYes, with built-in wait/webhook actions
Google Cloud WorkflowsPer step executed + external API callsWorkflows visual editor (more limited)Yes, with sleep/callback steps

The practical differentiator is ecosystem gravity, not raw feature parity. If your infrastructure already lives in Lambda, DynamoDB, and SQS, Step Functions' optimized service integrations save you a Lambda function on nearly every state. If you're deep in the Azure ecosystem with Office 365 and Dynamics connectors, Logic Apps' connector catalog does the same job for that stack. Nobody should migrate cloud providers just for the orchestration layer — pick based on where the rest of your infrastructure already lives.

Advanced Tips for Production Workloads

Use Express workflows nested inside Standard workflows. You can call an Express workflow as a Task from within a Standard workflow. This gets you the audit trail and long-duration support of Standard for the overall business process, while offloading a high-frequency inner loop to cheaper, faster Express execution.

Version your state machines with aliases. Step Functions supports versioning and aliases similar to Lambda, letting you shift traffic gradually between a new state machine definition and the old one — useful for testing a retry-logic change against 10% of production traffic before a full cutover.

Log to CloudWatch Logs at the ERROR level minimum, ALL level while debugging. Standard workflows log execution history automatically in the console, but wiring an explicit CloudWatch Logs destination gives you a queryable, retainable log stream you can run Logs Insights queries against — something the console's visual history doesn't support.

Redrive failed Map Runs instead of restarting the whole batch. Distributed Map supports redriving failed child executions within a Map Run — up to 1,000 redrives per the service quotas — so a batch job that fails on 40 out of 50,000 items doesn't require reprocessing the other 49,960 that already succeeded.

Security Hardening for Production State Machines

Step Functions state machines end up touching a lot of your account's surface area — Lambda, databases, queues, sometimes third-party APIs holding payment or customer data — so treating the execution role as an afterthought is a bigger mistake here than almost anywhere else in a serverless stack.

Scope every execution role to specific ARNs, never wildcards. The IAM example in Step 5 lists exact function and topic ARNs rather than Resource: "*". This matters more than it looks like it should: a state machine with a wildcard lambda:InvokeFunction permission can be tricked, via a compromised upstream input, into invoking a completely unrelated Lambda function elsewhere in the account if an attacker can influence the ARN passed into a dynamic Resource field.

Encrypt sensitive execution data. By default, Step Functions execution history and logs may contain the full input/output JSON of every state — which is exactly where a payment total, a customer email, or an internal auth token ends up if you're not careful. Step Functions supports customer-managed KMS keys for encrypting this data at rest; for any workflow touching PII or payment details, turn this on rather than relying on the default AWS-owned key.

Never pass secrets through state input/output. It's tempting to thread an API key or database password through the JSON payload between states. Don't — that data lands in CloudWatch Logs and the execution history, both of which are far more widely readable than a Secrets Manager entry. Have each Lambda function fetch its own credentials from Secrets Manager or Parameter Store at invocation time instead.

Use resource-based policies to restrict who can start executions. A state machine that processes financial transactions shouldn't be startable by every IAM principal in the account. Attach a resource policy (or scope IAM policies granting states:StartExecution) to the specific roles — a particular API Gateway integration role, a specific CI/CD pipeline role — that legitimately need to trigger it.

Cost Optimization: Keeping the Bill Predictable

Step Functions bills are rarely the biggest line item on an AWS invoice, but they're one of the easiest to accidentally blow up, because the cost scales with the number of states in your workflow, not just the number of times it runs. A Standard workflow with 20 states costs roughly four times what a 5-state workflow costs per execution, even if both process identical business logic — every state transition is a billable event.

Collapse trivial states. A Pass state that does nothing but reshape data, followed immediately by a Task state, can often be merged using Parameters or JSONata's Arguments field directly on the Task state, eliminating a transition (and its cost) entirely. Audit workflows with more than 15 states for Pass states that exist purely as a workaround for older JSONPath limitations — JSONata's inline expressions frequently make them unnecessary.

Match workflow type to actual duration and frequency, not habit. Teams that default to Standard workflows out of familiarity, then run them thousands of times a day for sub-minute jobs, are paying transition-based pricing for work that would cost less under Express's duration-based model. Conversely, forcing a multi-hour approval workflow into Express (which caps at 5 minutes) simply doesn't work — check the Standard vs. Express table earlier in this guide before defaulting to either.

Batch with Distributed Map instead of triggering one Standard execution per item. Starting 50,000 separate Standard workflow executions to process 50,000 rows in a CSV is far more expensive, and far harder to monitor, than one Distributed Map Run processing the same 50,000 rows as child executions of a single parent. The parent execution alone captures the aggregate success/failure count.

Set CloudWatch Logs retention explicitly. The default "never expire" retention on a log group quietly created for a high-volume Express workflow adds up in storage costs over a year of continuous execution. Thirty or ninety days of retention is enough for nearly every debugging scenario; anything you need to keep longer for compliance belongs in an S3 export with lifecycle rules, which is dramatically cheaper per GB than CloudWatch Logs storage.

Troubleshooting: 8 Issues You'll Actually Hit

1. Execution fails immediately with "States.Runtime" error. Usually a malformed ASL definition — check for a missing comma or an unreachable state (one nothing transitions into, or that has no End: true or Next field).

2. "AccessDeniedException" when a Task state invokes Lambda. The state machine's execution role is missing lambda:InvokeFunction for that specific function ARN. Check the exact ARN matches — a region or account ID typo in the IAM policy is the usual culprit.

3. Retry block never fires even though the Lambda is clearly erroring. The ErrorEquals list has to match the exact exception class name your Lambda raises. A generic Exception in your code won't match a Retry block scoped to PaymentGatewayTimeout — either raise the specific exception type or broaden the Retry to States.ALL.

4. Execution silently "SUCCEEDED" but downstream data looks wrong. Almost always a ResultPath/OutputPath misconfiguration overwriting expected fields. Click through the execution history in the console and inspect the input/output JSON at each individual state.

5. Distributed Map Run stuck at 0% progress. Check the ItemReader's S3 permissions first — the execution role needs s3:GetObject on the exact source bucket/key, not just a generic S3 read policy scoped to a different prefix.

6. "ExecutionLimitExceeded" on Standard workflow. You've hit the open-executions quota for the account/region (documented around 1,000,000 open Standard executions per account per region). This almost always means executions aren't completing and are piling up — check for a stuck Wait state or an un-timed-out callback token.

7. CDK deploy fails with a circular dependency between the state machine and its IAM role. Usually caused by referencing the state machine's own ARN inside a resource policy attached before the state machine exists. Split the resource policy into a separate construct applied after the state machine is created.

8. Local Step Functions Docker container returns "endpoint not found." Confirm the container's actually listening on 8083 with docker ps, and double-check every AWS CLI command includes --endpoint-url http://localhost:8083 — it's easy to forget on the second or third command in a session and get routed to real AWS instead.

Frequently Asked Questions

Is AWS Step Functions serverless?
Yes. There's no server to provision or patch — you pay per state transition (Standard) or per request/duration (Express), and AWS manages all underlying compute for the orchestration layer itself.

Do I need Lambda to use Step Functions?
No. Optimized service integrations let Step Functions call DynamoDB, SNS, SQS, EventBridge, ECS, and many other AWS services directly from a Task state without a Lambda function as an intermediary.

What's the difference between Standard and Express workflows in one sentence?
Standard workflows bill per state transition, run up to a year, and give exactly-once execution with full visual history; Express workflows bill per request and duration, cap out at 5 minutes, and trade exactly-once guarantees for much higher throughput at lower cost.

Can Step Functions call an external, non-AWS API?
Yes, through an HTTP Task state or by routing through a Lambda function or API Gateway, which is the more common pattern when you need custom auth headers or request signing beyond a plain HTTPS call.

How long can a Step Functions workflow run?
Standard workflows support executions up to one year, which is what makes the callback-token pattern for human approvals practical. Express workflows are capped at 5 minutes total.

Is JSONata mandatory for new state machines?
No. AWS documentation recommends JSONata for new state machines as the more readable option, but the older JSONPath-based Parameters/ResultPath approach still works and isn't being deprecated. Existing workflows must opt in explicitly to switch.

What happens if a Distributed Map child execution fails?
The Map Run continues processing other items; the failed item is recorded and can be redriven (retried) without reprocessing the items that already succeeded, up to the documented redrive quota.

Can I test a state machine without deploying to AWS?
Yes, with AWS Step Functions Local, a Docker container that emulates the state-transition logic on your machine. It won't validate real IAM permissions, so you still need a deployed test against a dev account before production.

Marcus Chen

Marcus Chen

Gaming & Consumer Tech Editor

Marcus Chen is a senior editor at Tech Insider, where he leads coverage of the US online gaming market, including sweepstakes and social casinos, alongside consumer technology. He evaluates operators on their published terms, licensing and RNG certifications, stated redemption policies, and corroborating independent reporting, and writes plainly about what the evidence supports. Tech Insider does not run first-party money tests and does not gamble with reader funds. Marcus has reported on the technology and online-gaming industries for more than a decade.

View all articles