← Back to DevBytes

Troubleshooting Step Functions: Common Issues and Solutions

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:

How Step Functions Reports Errors

Before diving into specific issues, it helps to understand where to look. Step Functions exposes several diagnostic surfaces:

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:

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:

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles