Introduction to Troubleshooting AWS Step Functions
AWS Step Functions is a serverless orchestration service that lets you coordinate multiple AWS services into business-critical workflows. While it abstracts away much of the complexity of distributed systems, workflows can still fail in subtle and frustrating ways. Understanding how to diagnose and resolve these failures is an essential skill for any developer building stateful, event-driven applications on AWS.
This tutorial walks through the most common issues you will encounter when working with Step Functions, including state machine definition errors, IAM permission problems, Lambda invocation failures, timeout misconfigurations, and data transformation pitfalls. For each issue, we provide a concrete explanation, a code example, and a practical solution.
Why Troubleshooting Step Functions Matters
Step Functions sits at the center of many production architectures. When a workflow fails, it often blocks downstream business processes, delays data pipelines, or breaks user-facing features. Because Step Functions integrates with services like Lambda, DynamoDB, SQS, SNS, and ECS, a single workflow can fail for reasons originating in any of those services.
Effective troubleshooting matters because:
- Failures are often silent — a workflow may appear to succeed while a branch silently fails and is caught by a fallback state.
- Costs accumulate — stuck or retrying workflows consume state transitions and downstream service calls.
- Debugging is non-linear — the root cause may live in the input payload, the IAM role, the target service, or the state machine definition itself.
- Production impact is high — orchestration failures often cascade across multiple systems.
How Step Functions Reports Errors
Before diving into specific issues, it helps to understand where to look. Step Functions exposes several diagnostic surfaces:
- Execution History — available in the console or via the
GetExecutionHistoryAPI, this is the single most useful debugging tool. - CloudWatch Logs — Lambda logs, service integration logs, and Step Functions execution logs (if enabled).
- Execution Input and Output — visible in the console and useful for verifying payload shape.
- State Machine Definition — Amazon States Language (ASL) errors are reported at creation or update time.
Common Issue 1: State Machine Definition Errors
The most basic class of problems comes from malformed Amazon States Language definitions. These are usually caught at creation time, but some logical errors only surface during execution.
Problem: Invalid JSONPath in InputPath or ResultPath
Step Functions uses JSONPath to select and filter data. A common mistake is using a JSONPath expression that does not match the input structure.
{
"StartAt": "ProcessOrder",
"States": {
"ProcessOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:ProcessOrder",
"InputPath": "$.order.details",
"ResultPath": "$.order.result.invalid.path",
"End": true
}
}
}
The ResultPath above contains a dot-separated segment that may not exist in the input, which can cause unexpected behavior. More critically, invalid JSONPath syntax will cause a validation error at creation time.
Solution
Validate your JSONPath expressions and test them against sample payloads. Use the Step Functions console's visual editor to simulate inputs. Here is a corrected version:
{
"StartAt": "ProcessOrder",
"States": {
"ProcessOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:ProcessOrder",
"InputPath": "$.order.details",
"ResultPath": "$.processingResult",
"End": true
}
}
}
Problem: Missing or Mismatched State References
Every state referenced in Next, Default, Catcher, or Retry must exist in the States block. A typo here will prevent the state machine from being created.
{
"StartAt": "ValidateInput",
"States": {
"ValidateInput": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:ValidateInput",
"Next": "ProcessData"
},
"ProcessDat": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:ProcessData",
"End": true
}
}
}
The Next value ProcessData does not match the state name ProcessDat. The fix is straightforward: ensure the names match exactly.
Common Issue 2: IAM Permission Errors
The IAM role assumed by your state machine must have permission to invoke every resource it touches. This is one of the most frequent causes of execution failures.
Problem: State Machine Cannot Invoke Lambda
If your execution fails with an error like AccessDeniedException, the execution role is likely missing the lambda:InvokeFunction permission.
Solution
Attach a policy that grants invocation rights to the specific Lambda functions your workflow uses. Avoid wildcard resources in production.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:ProcessOrder"
},
{
"Effect": "Allow",
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:ValidateInput"
}
]
}
For service integrations like DynamoDB, SQS, or SNS, add the corresponding actions such as dynamodb:PutItem, sqs:SendMessage, or sns:Publish. Always scope resources to the specific ARNs your workflow uses.
Problem: Cross-Account or Cross-Region Access
When invoking resources in another account or region, the trust policy of the target resource must also allow the Step Functions execution role. This is a two-sided configuration that developers often miss.
Common Issue 3: Lambda Function Failures
When a Lambda function invoked by Step Functions throws an error, the state machine captures the error and transitions according to your Catch and Retry configuration. Without proper handling, the execution fails immediately.
Problem: Unhandled Lambda Exceptions
Consider a Lambda function that throws a generic error:
exports.handler = async (event) => {
const orderId = event.orderId;
if (!orderId) {
throw new Error("Missing orderId");
}
return { status: "processed", orderId };
};
If the input lacks orderId, the Lambda throws Error: Missing orderId. Step Functions records this as States.TaskFailed with the error name Error. Without a Catch block, the entire execution fails.
Solution: Add Retry and Catch Blocks
{
"StartAt": "ProcessOrder",
"States": {
"ProcessOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:ProcessOrder",
"Retry": [
{
"ErrorEquals": ["States.TaskFailed"],
"IntervalSeconds": 2,
"MaxAttempts": 3,
"BackoffRate": 2.0
}
],
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"Next": "HandleFailure",
"ResultPath": "$.error"
}
],
"Next": "OrderComplete"
},
"HandleFailure": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:HandleFailure",
"End": true
},
"OrderComplete": {
"Type": "Succeed"
}
}
}
This configuration retries transient failures up to three times with exponential backoff, then routes persistent failures to a dedicated handler. Using States.ALL in the Catch block ensures no error goes unhandled.
Best Practice: Throw Typed Errors from Lambda
Generic Error objects make it hard to distinguish failure modes. Create custom error classes so your Catch blocks can route intelligently.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class ServiceUnavailableError extends Error {
constructor(message) {
super(message);
this.name = "ServiceUnavailableError";
}
}
exports.handler = async (event) => {
if (!event.orderId) {
throw new ValidationError("Missing orderId");
}
try {
const result = await callExternalService(event.orderId);
return { status: "processed", result };
} catch (err) {
throw new ServiceUnavailableError(err.message);
}
};
Now your state machine can catch validation errors separately from transient service errors:
"Catch": [
{
"ErrorEquals": ["ValidationError"],
"Next": "NotifyUserOfBadInput",
"ResultPath": "$.error"
},
{
"ErrorEquals": ["ServiceUnavailableError"],
"Next": "QueueForRetry",
"ResultPath": "$.error"
}
]
Common Issue 4: Timeouts
Timeouts are a frequent and often confusing source of failures. Step Functions has multiple timeout settings, and they interact in ways that can surprise developers.
Problem: Task Timed Out
The error States.Timeout occurs when a task exceeds its configured TimeoutSeconds or when the underlying service does not respond in time. By default, Lambda invocations through Step Functions have a service-level timeout, but you should always set explicit timeouts.
Solution: Configure Timeouts Explicitly
{
"StartAt": "LongRunningTask",
"States": {
"LongRunningTask": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:LongRunningTask",
"TimeoutSeconds": 300,
"HeartbeatSeconds": 60,
"Retry": [
{
"ErrorEquals": ["States.Timeout"],
"IntervalSeconds": 5,
"MaxAttempts": 2,
"BackoffRate": 2.0
}
],
"Next": "Done"
},
"Done": {
"Type": "Succeed"
}
}
}
Key points about timeouts:
TimeoutSecondsis the absolute maximum time a task can run.HeartbeatSecondsrequires the task to send periodic heartbeats. If a heartbeat is missed, the task is considered failed.- The Lambda function's own timeout must be less than or equal to the Step Functions
TimeoutSeconds. - For activities (not Lambda), heartbeats are mandatory if
HeartbeatSecondsis set.
Problem: Execution-Level Timeout
Entire executions can also time out. If you set TimeoutSeconds on the state machine itself, any execution exceeding that duration is automatically failed with States.Timeout. This is useful for preventing runaway workflows but can cause unexpected failures if set too low.
Common Issue 5: Payload Size Limits
Step Functions enforces a strict limit on the size of data passed between states. As of this writing, the maximum payload size is 256 KB. Exceeding this limit causes an immediate failure.
Problem: States.DataLimitExceeded
This error occurs when the input to a state, the output of a state, or the data stored in the execution context exceeds 256 KB. It commonly happens when a Lambda function returns a large dataset or when a DynamoDB Scan returns many items.
Solution: Use Amazon S3 for Large Payloads
Instead of passing large data through the state machine, store it in S3 and pass the reference. This pattern is sometimes called the "S3 pointer pattern."
// Lambda function that stores large data in S3
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
exports.handler = async (event) => {
const largeDataset = await fetchLargeDataset(event.query);
const params = {
Bucket: 'my-workflow-data-bucket',
Key: `datasets/${event.executionId}-${Date.now()}.json`,
Body: JSON.stringify(largeDataset),
ContentType: 'application/json'
};
const result = await s3.upload(params).promise();
return {
s3Bucket: result.Bucket,
s3Key: result.Key,
recordCount: largeDataset.length
};
};
The next state in your workflow receives only the S3 reference and can fetch the data when needed:
// Downstream Lambda that reads from S3
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
exports.handler = async (event) => {
const params = {
Bucket: event.s3Bucket,
Key: event.s3Key
};
const data = await s3.getObject(params).promise();
const dataset = JSON.parse(data.Body.toString());
const processed = dataset.map(record => ({
...record,
processed: true
}));
return { processedCount: processed.length };
};
Common Issue 6: Choice State Logic Errors
The Choice state routes execution based on input data. Logic errors here can cause workflows to take unexpected paths or fail entirely.
Problem: No Matching Choice Rule
If none of the choice rules match and no Default is specified, the execution fails with States.NoChoiceMatched.
{
"StartAt": "RouteByAmount",
"States": {
"RouteByAmount": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.amount",
"NumericGreaterThan": 1000,
"Next": "HighValueProcessing"
},
{
"Variable": "$.amount",
"NumericLessThan": 100,
"Next": "LowValueProcessing"
}
],
"Default": "StandardProcessing"
},
"HighValueProcessing": { "Type": "Succeed" },
"LowValueProcessing": { "Type": "Succeed" },
"StandardProcessing": { "Type": "Succeed" }
}
}
Without the Default field, an input with amount equal to 500 would cause the execution to fail. Always include a Default transition.
Problem: Type Mismatches in Comparisons
Choice rules are type-sensitive. Comparing a string variable with a numeric value, or using NumericGreaterThan on a string field, will cause runtime errors. Ensure your comparison operators match the data types in your payload.
Common Issue 7: Parallel and Map State Issues
Problem: Map State Iteration Failures
The Map state processes items in parallel. By default, if any iteration fails, the entire Map state fails. This is often undesirable for batch processing where partial success is acceptable.
Solution: Handle Errors Within Iterations
{
"StartAt": "ProcessBatch",
"States": {
"ProcessBatch": {
"Type": "Map",
"ItemsPath": "$.items",
"MaxConcurrency": 5,
"Iterator": {
"StartAt": "ProcessItem",
"States": {
"ProcessItem": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:ProcessItem",
"Retry": [
{
"ErrorEquals": ["States.TaskFailed"],
"IntervalSeconds": 2,
"MaxAttempts": 3,
"BackoffRate": 2.0
}
],
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"Next": "RecordFailure",
"ResultPath": "$.error"
}
],
"Next": "RecordSuccess"
},
"RecordSuccess": {
"Type": "Pass",
"Result": { "status": "success" },
"End": true
},
"RecordFailure": {
"Type": "Pass",
"Result": { "status": "failed" },
"End": true
}
}
},
"ResultPath": "$.results",
"Next": "Summarize"
},
"Summarize": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:Summarize",
"End": true
}
}
}
By catching errors within each iteration, the Map state completes successfully and returns a results array where each item indicates success or failure. A downstream state can then summarize the results.
Problem: Parallel State Branch Failures
In a Parallel state, all branches run concurrently. If any branch fails and is not caught, the entire Parallel state fails and other branches are not waited upon. Add Catch blocks to each branch if you need to tolerate individual branch failures.
Common Issue 8: Service Integration Errors
Step Functions can directly integrate with many AWS services without requiring Lambda. These integrations have their own error modes.
Problem: DynamoDB Conditional Check Failure
When using DynamoDB integration with a conditional expression, a failed condition returns DynamoDB.ConditionalCheckFailedException. Without a catch block, this fails the execution.
{
"StartAt": "UpdateItem",
"States": {
"UpdateItem": {
"Type": "Task",
"Resource": "arn:aws:states:::dynamodb:updateItem",
"Parameters": {
"TableName": "OrdersTable",
"Key": {
"orderId": { "S.$": "$.orderId" }
},
"UpdateExpression": "SET #status = :newStatus",
"ConditionExpression": "#status = :expectedStatus",
"ExpressionAttributeNames": {
"#status": "status"
},
"ExpressionAttributeValues": {
":newStatus": { "S": "processed" },
":expectedStatus": { "S": "pending" }
}
},
"Catch": [
{
"ErrorEquals": ["DynamoDB.ConditionalCheckFailedException"],
"Next": "HandleConflict",
"ResultPath": "$.error"
}
],
"Next": "Done"
},
"HandleConflict": {
"Type": "Pass",
"Result": "Item was already processed by another execution",
"End": true
},
"Done": {
"Type": "Succeed"
}
}
}
Problem: SQS SendMessage Permission or Configuration Errors
When integrating with SQS, ensure the execution role has sqs:SendMessage permission and that the queue URL is correctly specified. A common mistake is forgetting the MessageBody parameter or passing it as a static string instead of a JSONPath reference.
{
"Type": "Task",
"Resource": "arn:aws:states:::sqs:sendMessage",
"Parameters": {
"QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue",
"MessageBody.$": "$.messagePayload"
},
"Next": "NextState"
}
Best Practices for Troubleshooting Step Functions
Enable Execution Logging to CloudWatch
Step Functions can log execution details to CloudWatch Logs. Enable this at the state machine level with appropriate log levels.
aws stepfunctions create-state-machine \
--name MyStateMachine \
--definition file://definition.json \
--role-arn arn:aws:iam::123456789012:role/StepFunctionsRole \
--logging-configuration \
level=ALL,includeExecutionData=true,\
destinations=[{cloudWatchLogsLogGroup={logGroupArn=arn:aws:logs:us-east-1:123456789012:log-group:/stepfunctions/MyStateMachine:*}}]
Setting the level to ALL captures every event, which is invaluable during development. In production, consider ERROR or FATAL to reduce log volume and cost.
Use the GetExecutionHistory API Programmatically
For automated debugging or alerting, query execution history directly:
const AWS = require('aws-sdk');
const stepfunctions = new AWS.StepFunctions();
async function getFailedEvents(executionArn) {
const params = {
executionArn: executionArn,
reverseOrder: true
};
const history = await stepfunctions.getExecutionHistory(params).promise();
const failedEvents = history.events.filter(event =>
event.type.includes('Failed') ||
event.type.includes('TimedOut') ||
event.type.includes('Aborted')
);
return failedEvents.map(event => ({
type: event.type,
timestamp: event.timestamp,
details: event[stateMachineEventDetails(event.type)]
}));
}
function stateMachineEventDetails(type) {
if (type.includes('TaskFailed')) return 'taskFailedEventDetails';
if (type.includes('ExecutionFailed')) return 'executionFailedEventDetails';
if (type.includes('TimedOut')) return 'taskTimedOutEventDetails';
return 'stateEnteredEventDetails';
}
module.exports = { getFailedEvents };
Design for Idempotency
Because Step Functions retries failed tasks, every task should be idempotent. If a Lambda function is retried, it should produce the same result without side effects like duplicate database entries. Use idempotency keys or conditional writes to guard against duplicate processing.
exports.handler = async (event) => {
const idempotencyKey = event.idempotencyKey || event.orderId;
try {
await dynamodb.put({
TableName: 'ProcessedOrders',
Item: {
idempotencyKey: idempotencyKey,
orderId: event.orderId,
status: 'processed',
processedAt: new Date().toISOString()
},
ConditionExpression: 'attribute_not_exists(idempotencyKey)'
}).promise();
} catch (err) {
if (err.code === 'ConditionalCheckFailedException') {
return { status: 'already_processed', orderId: event.orderId };
}
throw err;
}
return { status: 'processed', orderId: event.orderId };
};
Use Test State for Debugging
The Step Functions console provides a "Test state" feature that lets you run a single state with sample input without starting a full execution. This is extremely useful for isolating issues in specific states. You can also use the TestState API:
aws stepfunctions test-state \
--definition '{"Type":"Task","Resource":"arn:aws:lambda:us-east-1:123456789012:function:ProcessOrder","End":true}' \
--role-arn arn:aws:iam::123456789012:role/StepFunctionsRole \
--input '{"orderId":"12345","amount":500}'
Structure Workflows for Observability
Insert Pass states at key points to log intermediate data. Use descriptive state names. Break large workflows into smaller, nested state machines using the StateMachine integration (available with Step Functions support for nested workflows) to keep individual executions manageable and debuggable.
Monitor with CloudWatch Alarms
Set up alarms on key Step Functions metrics:
ExecutionsFailed— alert when failures exceed a threshold.ExecutionThrottled— indicates you are hitting concurrency limits.ExecutionTime— detect slow-running workflows.ProvisionedRefillRate— monitor for throttling on service integrations.
aws cloudwatch put-metric-alarm \
--alarm-name StepFunctions-FailureRate \
--alarm-description "Alert on Step Functions execution failures" \
--metric-name ExecutionsFailed \
--namespace AWS/States \
--statistic Sum \
--period 300 \
--threshold 5 \
--comparison-operator GreaterThanThreshold \
--dimensions Name=StateMachineArn,Value=arn:aws:states:us-east-1:123456789012:stateMachine:MyStateMachine \
--evaluation-periods 1 \
--alarm-actions arn:aws:sns:us-east-1:123456789012:StepFunctionsAlerts
Conclusion
Troubleshooting AWS Step Functions requires a systematic approach that spans the state machine definition, IAM configuration, target service behavior, and payload management. By understanding the common failure modes covered in this tutorial — definition errors, permission issues, Lambda exceptions, timeouts, payload limits, choice logic, parallel and map state handling, and service integration errors — you can diagnose problems quickly and build more resilient workflows. The most effective debugging strategy combines the execution history API, CloudWatch logging, idempotent task design, and the Test State feature to isolate and resolve issues before they reach production. With these tools and practices, you can confidently operate Step Functions workflows at scale and keep your orchestration layers reliable and observable.