10 minute read

In my previous post I built the same order workflow twice, once as a Lambda durable function and once as a Step Functions state machine, and went through the differences between them. This post is the second part. Instead of comparing features, I went through the use cases I could think of, and for each one I tried to say which of the two I would use and why.

AWS describes the difference quite well in their own comparison. Durable functions are meant for application development inside Lambda, and Step Functions for orchestrating workflows across AWS services. Please note that there is a lot of overlap between the two. Both can run for up to a year, both can wait without paying for compute, both retry failed work and both can pause until a human approves something. So for most workflows either of them would work, and the decision mostly comes down to where your logic lives and who is going to maintain it.

Where durable functions fit

The use case AWS lists first for durable functions is distributed transactions, and I think it is also the best fit. Think of an order, a booking or a payment that goes through several systems, where everything that already succeeded has to be undone when a later step fails. In a durable function the compensation is a try and catch around your steps and the state of the transaction is simply the variables in your function, so the business rules stay right next to the orchestration. You can build the same thing in Step Functions, but every compensation becomes another state with another Catch, and the decisions end up split between the graph definition and a handful of Lambda functions. The one thing you have to get right is idempotency. A step can run again if the function gets interrupted, so anything that charges money or sends a message should use at most once semantics or an idempotency key. I went into the details of replay in the previous post, and I would read up on it before building something like this.

AI agents are the second one, and I would not be surprised if this is where durable functions end up being used the most. An agent loop is a model call, then a tool call depending on the answer, then another model call, sometimes with a human approving something in between, and that repeats until the agent is done. The control flow depends on every response and it changes a lot while you are building it, which is much easier to handle in code than in a graph. In Step Functions you would model the loop with Choice states and pass the conversation around as JSON from one state to the next. With a durable function every model call goes into a step, so it is checkpointed and you do not pay for it a second time when the function replays, and the human approval is a waitForCallback that costs nothing while it waits. The limits were also raised in October 2026 to 1 MB per operation and 30,000 operations per execution, which helps when you carry a long conversation history around.

Then there are business processes that mostly wait, like employee onboarding, loan approvals, or a trial that sends a reminder after 13 days and downgrades the account after 14. With a durable function that is a context.wait with the number of days, or a callback when a person has to do something, and the function is suspended in between. You would usually run one execution per user or per application, and the code reads very close to how you would describe the process to someone. To be fair, Step Functions handles these just as well, and if the process is owned by people who want to look at it as a diagram I would go that way. If it is a feature in your product that developers own, I would keep it in code.

A smaller one is polling, for example waiting for a third party API to finish a job or for a file to show up somewhere. waitForCondition checks a condition, waits and checks again, with a checkpoint after every attempt and no compute billed while it waits. In Step Functions the same loop is a Wait state, a task and a Choice that goes back to the Wait, which works fine, but it is quite a lot of ceremony for something that is a few lines of code.

Last but not least, this depends on the team as much as on the use case. If your developers write TypeScript, Python, Java, Go or .NET every day, want to unit test their workflows with the framework they already use, and let an AI coding assistant write a good part of the code, durable functions fit the way they already work. It is basically the same argument I made for CDK for Terraform back in 2022, developers should be able to use the language they know instead of learning a configuration language for one part of the system.

Where Step Functions fits

If most of the work in your workflow is done by AWS services and not by your own code, I would use Step Functions. The SDK integrations cover over two hundred services and thousands of API actions, and a state can call them directly without a Lambda function in between. So writing to DynamoDB, sending a message to SQS, publishing to SNS or calling Bedrock is configuration in your state machine. In a durable function each of these is SDK code inside a step, which you have to write and maintain yourself.

Data pipelines are the use case closest to my own work, and here I would go with Step Functions. A typical pipeline starts a Glue job, waits for it to finish, runs an Athena query, maybe adds an EMR step and writes the result somewhere. For this Step Functions has the run a job pattern, .sync, which starts the job and waits until it is done. It is available for Glue, Athena, EMR, Batch, ECS, EKS, SageMaker and CodeBuild, so you do not write any polling code at all. With a durable function you would have to build the waiting yourself, either with waitForCondition or with an EventBridge event that completes a callback. That works, but you would be rebuilding something Step Functions already gives you. Machine learning pipelines are very similar, a training job, an evaluation, a decision based on the metric and a deployment, all of them AWS service calls with a .sync integration, and when a run fails the graph shows you where.

Distributed Map is another big reason. It can read millions of objects from S3 and process them with up to 10,000 parallel child executions, each of them its own workflow, and it tolerates failures and lets you redrive only the items that failed. Durable functions have map and parallel as well, but they run inside the same function invocation using child contexts and do not start separate functions. That is fine for a list of a few dozen or a few hundred items, but I would not use it for a whole bucket full of files.

IT and security automation is a good fit too. Say an EventBridge rule fires because of a GuardDuty finding or because somebody made an S3 bucket public. The workflow tags the resource, calls Systems Manager to isolate an instance, opens a ticket and waits for someone from the security team to confirm before it remediates anything. Almost every step there is an AWS API call, the people who own the workflow are usually operations or security engineers rather than application developers, and an auditor will want to know what happened and in which order. Step Functions Standard keeps the execution history for 90 days, shows it as a graph and runs every state exactly once, and for this kind of automation I think that matters more than how nice the code is.

Visibility in general was my main argument for Step Functions in 2021, and I still think it is valid. If business analysts, auditors or another team need to understand and sign off on the logic, a graph in Workflow Studio is a lot easier to discuss than a TypeScript file. I would just be honest about how often that actually happens in your organisation, because it is easy to use it as an argument even when nobody outside the team ever opens the console.

Express workflows are a separate case. They run for at most five minutes, are at least once, are priced per execution and duration instead of per state transition, and can start up to 100,000 executions per second. For streaming data, IoT ingestion or transforming events between services at that rate, Express is what I would use, and durable functions are not really built for that kind of workload.

And finally, Step Functions does not care what does the actual work. A step can be a Lambda function in any runtime, a container on ECS, a job on EMR or an activity, which is a worker that polls Step Functions for tasks and can run anywhere, also on premises. There is also no runtime to patch. A durable function is a Lambda function, so upgrading the runtime is your job, and since executions can run for months, the new version also has to be able to replay the checkpoints of the old one.

The grey areas

For some use cases I do not see a clear winner, and there I would go with whatever the team already knows. Human approval is the obvious example. Step Functions has had task tokens for years, durable functions have callbacks, and in practice the two work almost the same way. Short sequences of three or four Lambda functions are similar, both will do the job, both cost very little and the effort is about the same.

Cost in general only starts to matter at high volume or with workflows that have a lot of small states. In the previous post I calculated roughly $58 per million orders for the durable version and roughly $175 for the Step Functions Standard version of the same flow, but at a few thousand executions a month neither of them will make a real difference on your bill.

Using both

For bigger systems I would not choose between them at all. AWS describes a hybrid pattern in their documentation, where Step Functions coordinates the high level process across services and durable functions handle the application logic inside one of its steps, and I think that is where most larger setups will end up. To give an example from the data side, imagine a nightly pipeline in Step Functions that loads new documents from S3, cleans them with a Glue job and goes through the results with Distributed Map. For each document one of the steps calls a durable function, which runs an AI agent that classifies and enriches the document with a few model calls, a lookup and, when the model is not confident, a human review. The pipeline stays visible and close to the AWS services, and the agent logic stays in code, where developers can test it and change it.

How I would decide

If I had to boil it down, I would ask three questions. Is most of the work done by your own code or by AWS services? Is it maintained by developers who live in their IDE, or by a mix of people who need to see a diagram? And do you need something only Step Functions has, like .sync jobs, Distributed Map or Express workflows? If the answers are your own code, developers and no, I would go with durable functions. Otherwise I would go with Step Functions.

For existing workflows, AWS recommends keeping Step Functions for established cross service workflows and considering durable functions for new Lambda logic that needs to be reliable, and I agree with that. I would not migrate a state machine that works fine just because durable functions are available now. For new application logic in Lambda though, I would start with a durable function.

Please note that all of this is based on what I could verify in the AWS documentation in November 2026. Durable functions in particular are still changing quickly, so check the current limits before you rely on them. And if you went with one of the two for a use case and regretted it later, I would really like to hear about it.

Thank you very much for reading this post. Pedram