On July 23, 2026, AWS announced the general availability of the Lambda Durable Execution SDK for .NET.
For .NET developers, AWS Step Functions has been the usual AWS-managed choice for long-running orchestration. Lambda durable functions now provide another option: keeping the workflow in C# alongside the application logic.
This article uses an order-processing workflow written in C# to explain how Lambda durable functions work in .NET, including checkpointing, replay, callbacks, idempotency, timeouts, and retries.
The complete sample is available in the ssathasivan/LambdaDurablefuctionsDotNet repository.
Example workflow
Consider an order-processing workflow with the following rules:
- An order arrives in the system.
- If the order value is $500 or less, process it immediately.
- If the order value exceeds $500, require manager approval.
- If the manager declines, stop processing.
- If nobody responds within three days, expire the order.
A graphical representation of the workflow is shown below.
validate order
|
amount > $500? ----------------------+
| |
no yes
| |
| wait for manager callback
| |
| declined or timeout -> stop
| |
| approved
| |
+--------------+---------------+
|
complete order
|
completed
Architecture
The application flow is:
- API Gateway ->
OrderApi-> stores the order in DynamoDB and startsOrderWorkflow. -
OrderWorkflow-> validates the order and waits for approval when required. - Manager approval ->
OrderApi-> resumesOrderWorkflowusing the callback ID. -
OrderWorkflow-> DynamoDB -> records the final order status.
Before looking at how the workflow runs, it helps to understand a few terms used by Lambda durable functions.
Key terms
A durable execution may involve multiple Lambda invocations. These terms are important when reasoning about the workflow:
| Term | What it means |
|---|---|
| Durable execution | One end-to-end run of the workflow, from the initial invocation to the final result. Lambda may use multiple function invocations to complete it, and the execution can remain active for up to one year. |
| Durable operation | A workflow action performed through the durable context rather than as a regular method call. Durable operations manage work across multiple invocations. Refer to the Durable operations table below for the full list. |
| Checkpoint | A persisted record associated with a durable operation, containing information such as its type, name, input, result, error, and status. Lambda uses the checkpoint log to reconstruct execution state during replay. |
| Suspend and resume | When the workflow waits for a timer, callback, or retry delay, the current invocation can end and later resume in another invocation. Lambda starts another invocation when the workflow is ready to continue. |
| Replay | When an execution resumes, the handler runs again from the beginning. Completed durable operations return their checkpointed state or results instead of repeating completed work. |
Durable operations
This workflow uses two durable operations: StepAsync and WaitForCallbackAsync. The .NET SDK provides the following operations:
| .NET API | Purpose |
|---|---|
CreateCallbackAsync |
Create a callback for an external system to complete. |
InvokeAsync |
Invoke another durable Lambda function and await its result. |
MapAsync |
Apply a function to each item, with optional concurrency control. Item processing can be retried or replayed. |
ParallelAsync |
Execute multiple named branches concurrently. |
RunInChildContextAsync |
Run a user function inside a sub-workflow. |
StepAsync |
Execute a step with automatic checkpointing. |
WaitAsync |
Suspend until a timer expires. |
WaitForCallbackAsync |
Suspend until an external system responds. |
WaitForConditionAsync |
Poll a condition with durable delays. |
Timeouts
A durable function has two separate timeouts.
The function timeout applies to a single Lambda invocation. It is the same setting a standard Lambda function has, and it still has the same maximum of 15 minutes.
The execution timeout applies to the whole durable execution, from the first invocation to the final result, including waits, retries, callbacks, and replay. It can be set up to one year, and it defaults to 24 hours.
Managers have up to three days to respond. A Lambda invocation can run for only 15 minutes, so the workflow suspends while it waits and resumes when a decision arrives. The default execution timeout is not long enough for this scenario, so the CDK stack increases it to seven days:
var function = new Function(this, "OrderWorkflowFunction", new FunctionProps
{
Timeout = Duration.Seconds(30), // One Lambda invocation
DurableConfig = new DurableConfig
{
ExecutionTimeout = Duration.Days(7), // The complete workflow
RetentionPeriod = Duration.Days(7), // History after completion
},
});
Implementation
The excerpts below focus on the durable workflow and the parts that handle callbacks and idempotency.
Durable workflow
The workflow is written as sequential C#. Work that interacts with an external system runs inside a durable operation so Lambda can checkpoint its result.
The following excerpt is from OrderFunction. Its Lambda handler passes WorkflowAsync to DurableFunction.WrapAsync<OrderRequest, OrderResult>.
private const decimal ManualApprovalThreshold = 500m;
public static async Task<OrderResult> WorkflowAsync(
OrderRequest order,
IDurableContext ctx)
{
// The validation result is checkpointed. During replay, a completed step
// returns the stored result instead of validating the order again.
var validation = await ctx.StepAsync(
async (_, ct) => await CatalogService.ValidateAsync(order, ct),
name: "validate-order");
if (!validation.IsValid)
{
return new OrderResult(
order.OrderId,
"Rejected",
RejectionReason: validation.Reason);
}
if (order.Amount > ManualApprovalThreshold)
{
ApprovalDecision decision;
try
{
decision = await ctx.WaitForCallbackAsync<ApprovalDecision>(
async (callbackId, _, ct) =>
await OrderService.RecordAwaitingApprovalAsync(
order.OrderId,
callbackId,
ct),
name: "manager-approval",
config: new WaitForCallbackConfig
{
Timeout = TimeSpan.FromDays(3),
// This retries the callback submitter (the DynamoDB write),
// not the three-day wait.
RetryStrategy = RetryStrategy.Exponential(maxAttempts: 3),
});
}
catch (CallbackTimeoutException)
{
return new OrderResult(
order.OrderId,
"Expired",
RejectionReason: "No approval within 3 days");
}
if (!decision.Approved)
{
return new OrderResult(
order.OrderId,
"Rejected",
RejectionReason: $"Declined by {decision.ApprovedBy}");
}
}
// A step body may run more than once because of retries or an interruption
// before its result is checkpointed. The downstream service must enforce
// the idempotency key.
var confirmation = await ctx.StepAsync(
async (_, ct) => await OrderService.CompleteAsync(
order.OrderId,
idempotencyKey: order.OrderId,
ct: ct),
name: "complete-order",
config: new StepConfig
{
// This example retries every exception for brevity. A production
// workflow should retry only transient failures.
RetryStrategy = RetryStrategy.Exponential(
maxAttempts: 3,
initialDelay: TimeSpan.FromSeconds(2)),
});
return new OrderResult(
order.OrderId,
"Completed",
confirmation.ConfirmationId);
}
The status-writing steps and supporting models are omitted here. They are available in the repository.
WaitForCallbackAsync creates the callback and runs the submitter that stores its ID in DynamoDB. The retry strategy applies to that submitter, not to the three-day wait.
Idempotent completion
A step may run again if it is retried or interrupted before its result is checkpointed. The completion update therefore uses a DynamoDB condition:
try
{
await Dynamo.UpdateItemAsync(new UpdateItemRequest
{
TableName = table,
Key = Key(orderId),
UpdateExpression =
"SET #status = :completed, ConfirmationId = :cid",
ConditionExpression = "attribute_not_exists(ConfirmationId)",
ExpressionAttributeNames =
new Dictionary<string, string> { ["#status"] = "Status" },
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
[":completed"] = new() { S = "Completed" },
[":cid"] = new() { S = confirmationId },
},
}, ct);
}
catch (ConditionalCheckFailedException)
{
// The order was already completed. Return the stored confirmation.
var existing = await Dynamo.GetItemAsync(new GetItemRequest
{
TableName = table,
Key = Key(orderId),
ProjectionExpression = "ConfirmationId",
}, ct);
confirmationId = existing.Item["ConfirmationId"].S;
}
The first attempt stores the confirmation. If the step runs again, the condition fails and the existing confirmation is returned.
Starting and resuming the workflow
The API Lambda starts a durable execution with an asynchronous invoke. It uses the order ID as the durable execution name, giving repeated submissions a stable idempotency key:
var invoke = new InvokeRequest
{
FunctionName = DurableFunctionArn,
InvocationType = InvocationType.Event,
DurableExecutionName = orderId,
Payload = JsonSerializer.Serialize(workflowInput, Json),
};
await Lambda.InvokeAsync(invoke);
Durable functions must be invoked through a version or alias. Using an alias also keeps new executions pinned to a published function version.
When a manager responds, the API Lambda sends the decision back as the callback result:
var decision = new ApprovalDecision(
body.Approved,
body.ApprovedBy,
body.Comment);
var resultJson = JsonSerializer.Serialize(decision, Json);
await Lambda.SendDurableExecutionCallbackSuccessAsync(
new SendDurableExecutionCallbackSuccessRequest
{
CallbackId = body.CallbackId,
Result = new MemoryStream(Encoding.UTF8.GetBytes(resultJson)),
});
Both approval and denial use the callback-success API because the callback itself completed successfully. The workflow interprets the Approved property as the business decision. The callback-failure API is reserved for cases where the external approval operation itself failed.
How Lambda durable functions run the code
One durable execution is one run of the business workflow, but it may span multiple Lambda invocations. Waits, retry delays, callbacks, and runtime interruptions can all end the current invocation and cause Lambda to start another one later.
The order amount determines the normal path through this example.
A small order: one invocation on the normal path
An order for $120 arrives. Lambda calls Handler, which passes WorkflowAsync to WrapAsync, and the workflow runs from top to bottom without suspending:
-
validate-orderexecutes, and the SDK checkpoints theValidationResult. -
order.Amount > ManualApprovalThresholdis false, so the approval block is skipped. -
complete-orderexecutes, and itsOrderConfirmationis checkpointed. - The workflow returns
Completed.
Nothing needs to wait, so the durable execution normally completes in one invocation. Checkpoints are still written, but no replay is required on this path.
Small order ($120) - normal path
--------------------------------
validate-order -> execute, checkpoint result
amount <= $500 -> skip approval
complete-order -> execute, checkpoint result
return Completed
A large order: suspend and resume
An order for $4,000 takes the approval branch. A single Lambda invocation can run for at most 15 minutes, so a manager who may respond in three days cannot be waited for inside one invocation. The workflow suspends instead.
On the first invocation:
-
validate-orderexecutes and is checkpointed. -
manager-approvalcreates a callback ID. - The callback submitter records the callback ID and
AwaitingApprovalstatus in DynamoDB. - The durable execution suspends, and the current invocation ends.
The current invocation ends while the workflow waits, and the idle wait does not incur Lambda compute charges.
When the manager responds through the API, the API Lambda calls SendDurableExecutionCallbackSuccess. Lambda then starts another durable-function invocation and runs WorkflowAsync from the beginning:
-
validate-orderreturns its checkpointed result without callingCatalogService.ValidateAsyncagain. -
manager-approvalreturns the callback result. - If the manager approved the order,
complete-orderis the first operation without a terminal checkpoint, so it executes and stores its result. - The workflow returns
Completed.
Large order ($4,000) - approved path
------------------------------------
First invocation
validate-order -> execute, checkpoint result
manager-approval -> create callback, record AwaitingApproval, suspend
... hours or days pass without compute charges ...
API Lambda
approval request -> send callback result
Resumed invocation
validate-order -> return checkpointed result
manager-approval -> return callback result: approved
complete-order -> execute, checkpoint result
return Completed
A timeout follows a different resumed path. When the three-day callback timeout expires, WaitForCallbackAsync raises CallbackTimeoutException, and the workflow returns Expired. It does not continue to complete-order.
Large order ($4,000) - timeout path
-----------------------------------
First invocation
validate-order -> execute, checkpoint result
manager-approval -> create callback, record AwaitingApproval, suspend
... three days pass without a response ...
Resumed invocation
validate-order -> return checkpointed result
manager-approval -> callback timeout
return Expired
Replay
During replay:
- Ordinary C# between durable operations runs again.
- Completed operations return their checkpointed results.
- Operation bodies may still run more than once because of retries or interruptions.
This means conditions such as if (order.Amount > ManualApprovalThreshold) are evaluated again when the workflow resumes. Workflow code must therefore remain deterministic and produce the same sequence of durable operations for the same input and checkpoint history.
See Determinism during replay for more information.
Design gotchas
Lambda durable functions handle checkpoints, replay, and waits. Your application still owns these concerns:
-
Idempotency: A checkpoint does not provide exactly-once side-effect semantics. The sample uses the order ID at two boundaries: as
DurableExecutionNameto avoid starting duplicate workflows, and in a DynamoDB condition to avoid completing an order twice.
ConditionExpression = "attribute_not_exists(ConfirmationId)"
- Replay safety: Ordinary workflow code runs again during replay. Capture changing values such as time, GUIDs, random numbers, and feature-flag results inside durable operations.
var createdAt = await ctx.StepAsync(
(_, _) => Task.FromResult(DateTimeOffset.UtcNow),
name: "capture-created-time");
Retries: Retry only transient failures and make every retried side effect idempotent. In the callback configuration, the retry strategy applies to the submitter that stores the callback ID; it does not retry or extend the wait itself.
Callback security: Treat a callback ID as a secret capability. The sample returns it from the status endpoint to keep the design self-contained, but a production API needs authentication, authorization, audit logging, and protection against leaking the ID.
Timeout alignment: The callback timeout must fit inside the durable execution timeout. Also consider the lifetime of external tickets, approval links, and any data the workflow expects to read after it resumes.
Versioning: Operation names are part of persisted execution history. Renaming
manager-approvalor changing the order of operations can break replay for in-flight executions. Publish numbered versions and invoke through an alias.Operation boundaries: Use durable operations for external work, waits, retries, or values that must survive across invocations. Keep pure, deterministic decisions as normal C#.
Durable functions reduce orchestration code, but idempotency, security, consistency, and workflow evolution remain application responsibilities.
Sources
- AWS Lambda durable execution SDK for .NET is now generally available
- Lambda durable functions
- Basic concepts for Lambda durable functions
- Configure Lambda durable functions
- Deploy Lambda durable functions with Infrastructure as Code
- Best practices for Lambda durable functions
- Retries for Lambda durable functions
- Step operation reference
- Determinism during replay
- AWS::Lambda::Function CloudFormation reference
- AWS Lambda FAQs
- AWS Lambda pricing

Top comments (1)
Thanks for sharing the article with an example and the Github repository. Are you planning to deploy any Lambda Durable function in production.