Step Functions or Lambda Durable Functions?

In my state-machine series, we explored choices, waits and parallel operations using AWS Step Functions - specifically defining the structure of these in AWS CDK.
Agentic development gives us another reason for revisiting those concepts: coordinating work that might fail, invoking retries, or checkpointing and pausing when something needs a review.
AWS Lambda durable functions gives us another way to coordinate this orchestration. In this post, I’m taking a small workflow example and how we might define it in both patterns.
This post is more of an architectural walkthrough for the purposes of comparison, not so much a deployment guide.
What are we building?
The example that I’ll use today is something fairly trivial, it’s more conceptual rather than concrete. If you take any agentic process currently, there will be some form of human-in-the-loop (HITL) aspect to it - whether it’s explicitly during the runtime of the agent, or subsequently after for manual review.
This is a pattern that is becoming more embedded into the workflows that we deal with on a daily basis. This is exactly where technical implementation of orchestrating the pause/resume comes in to play.
If we take the example of a software development workflow using agentic processes now:
Add category filtering to the recipes API, include unit test coverage and update documentation
With this example, we want the agent to take care of the planning, implementation, and verification phases whilst the human then enters for the review phase. The agent performs the work, and the workflow decides when that work can proceed.
Take a look: Step Functions
We’ll take Step Functions as our baseline since I’ve already written about how to build these workflow definitions using CDK in a previous series. Let’s assume that we already have a standard step function definition in place, and we are focusing on implementing the callback behaviour for the HITL review phase.
The following snippet showcases how we might achieve that using a task definition, including task tokens for the callback:
import { Duration } from "aws-cdk-lib";
import * as sfn from "aws-cdk-lib/aws-stepfunctions";
import * as tasks from "aws-cdk-lib/aws-stepfunctions-tasks";
const approval = new tasks.LambdaInvoke(this, "Request approval", {
lambdaFunction: approvalRequestFunction,
integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN,
payload: sfn.TaskInput.fromObject({
taskToken: sfn.JsonPath.taskToken,
artifact: sfn.JsonPath.stringAt("$.artifact"),
}),
resultPath: "$.approval",
taskTimeout: sfn.Timeout.duration(Duration.hours(24)),
});
The Lambda records the task token and returns. We then would have some form of endpoint/interface elsewhere that subsequently calls SendTaskSuccess with the decision, or SendTaskFailure if something went wrong. This request would then resume the state machine, and we can use that decision in further logical operations within the state machine.
You can see that this example leans into the implementation detail of the Step Functions API, and relies on it for driving that workflow.
Take a look: Durable Execution Lambda
In contrast, the durable execution SDK allows us to define the same checkpointing/pausing/resuming functionality within a Lambda handler itself. The API defines the checkpoints in named steps. Below is the equivalent approval boundary inside a Lambda function:
import {
DurableContext,
withDurableExecution,
} from "@aws/durable-execution-sdk-js";
export const handler = withDurableExecution(
async (event: { artifact: string }, context: DurableContext) => {
const decision = await context.waitForCallback<string>(
"human-approval",
async (callbackId) => {
await recordReviewRequest(event.artifact, callbackId);
},
{ timeout: { seconds: 86400 } },
);
if (decision !== "approved") return { status: "rejected" };
return context.step("start-dev-deployment", async () => {
return startDeployment(event.artifact);
});
},
);
The review backend (same example endpoint/interface as above) would then use the SendDurableExecutionCallbackSuccess API to return the decision. The startDeployment method call would then return another reference, callback or another checkpointed operation.
The key difference here is that we’re relying on a separate API embedded within Lambda to handle the checkpointing/pause/resume here, but that it is directly embedded within the Lambda runtime. Meaning, you still have flexibility and control over the logic inside the function - which is pretty powerful for certain use cases.
One critical thing to remember with this approach is that you are still bound by the Lambda 15 minute (max) limit whilst it is actively executing. Durable execution can span numerous invocations, but each invocation has that same 15 minute limit.
What happens during the resume phase?
With durable execution in Lambdas, whenever it resumes, it executes the handler from the beginning again. Completed checkpointed operations return persisted results, and code outside those operations executes again - meaning the workflow decisions must remain deterministic. This pattern is different from simply continuing a paused JS process.
Checkpoints also don’t make exactly-once external writes. A branch creation could succeed before its result is captured, and checkpointed. Meaning you must ensure that your processes are idempotent, or can handle the retries in such a way that it recognises the existing branch.
This is definitely a tradeoff that you would need to consider when choosing between step functions and Lambda durable execution. Task tokens handles this somewhat out of the box for you, in the sense that you just need to pass that already minted task token around. With durable execution, you need to handle and persist that idempotentency token yourself and bake that into the design of your function/workflow.
Which would I choose?
I feel like both patterns and approaches definitely have their place. Whilst they fundamentally achieve the same thing in our example above, there are a few tradeoffs and considerations that need to be taken into account.
| Consideration | Step Functions Standard | Lambda durable functions |
|---|---|---|
| Workflow definition | CDK-generated state machine | TypeScript handler |
| Review visibility | Explicit execution graph | Durable execution and step views |
| Cost model | State transitions, plus invoked services | Active compute, durable operations and retained state |
Personally, for something well defined and structured, I’d lean towards Step Functions. Having the infrastructure take care of a lot of the heavy lifting is great, and the features supported are more than sufficient for 90% of use cases.
For something more prototypal, or likely to change requiring the flexibility, I’d go for Lambda durable functions.
Saying that though, if the durable function then became more structured and stable within the system, I’d probably opt for migrating it across to a Step Function state machine!
Conclusion
A very quick comparison of two execution models that can achieve the same functionality/pattern that is becoming the norm these days in our agentic world. It provides good confidence that your workflow orchestration architecture is not a one-way decision, and you can weigh up different options based on your use cases.