Distributed workflows rarely fail in one simple way.
A Lambda function may successfully call one service and then fail while calling the next. A downstream API may become unavailable for several minutes. A database transaction may time out. A worker may be interrupted after completing part of a business operation. An event may be delivered again after a timeout.
For a simple function, retrying the entire invocation may be enough. For a multi-step business process, it can become expensive and unreliable because previously completed work may be repeated.
This is where durable workflows become useful.
A durable workflow preserves enough execution state to resume work after failures instead of restarting the entire process from the beginning. For .NET teams building serverless applications, the interesting engineering question is not simply whether durable workflows recover from failures. It is how much recovery time, compute, storage, orchestration overhead, and operational complexity they introduce compared with conventional Lambda retries.
That question requires benchmarking.
This article presents a practical way to compare conventional Lambda retry-based recovery with durable workflow execution for .NET workloads. The focus is on measurable behavior: recovery latency, repeated work, throughput, failure handling, storage overhead, concurrency, and cost.
Why Durable Workflow Benchmarking Matters
Consider a five-step business process:
Create Order
|
v
Reserve Inventory
|
v
Process Payment
|
v
Create Shipment
|
v
Send Notification
Suppose the payment step fails.
With a conventional Lambda implementation, the entire function may be retried:
Create Order
Reserve Inventory
Process Payment -> Failure
Retry
Create Order
Reserve Inventory
Process Payment
Create Shipment
Send Notification
Some operations may execute twice.
A durable workflow can instead preserve the completed steps:
Create Order -> Completed
Reserve Inventory -> Completed
Process Payment -> Failed
|
v
Resume here
|
v
Create Shipment
Send Notification
This can reduce duplicated work, but it introduces orchestration and state-management overhead.
The benchmark needs to measure both sides.
Define the Two Architectures
Before measuring anything, create two implementations of the same business workflow.
Conventional Lambda Retry
The first implementation can use a normal Lambda handler.
public async Task Handler(
OrderRequest request,
CancellationToken cancellationToken)
{
await CreateOrderAsync(request, cancellationToken);
await ReserveInventoryAsync(request, cancellationToken);
await ProcessPaymentAsync(request, cancellationToken);
await CreateShipmentAsync(request, cancellationToken);
await SendNotificationAsync(request, cancellationToken);
}
If the function fails, the surrounding event or invocation mechanism may retry the Lambda.
The entire handler can therefore execute again.
Durable Workflow
The second implementation separates the workflow into recoverable steps.
Conceptually:
Workflow
|
+--> CreateOrder
|
+--> ReserveInventory
|
+--> ProcessPayment
|
+--> CreateShipment
|
+--> SendNotification
The orchestration layer maintains enough state to know which activities have completed.
The exact AWS implementation should be selected based on the application's requirements and the durable workflow capability being evaluated. The benchmark methodology remains the same: compare equivalent business workflows under identical failure conditions.
Define the Benchmark Questions
A useful benchmark should answer specific questions.
For example:
How long does a successful workflow take?
How long does recovery take after a transient failure?
How much work is repeated?
How many downstream requests are generated?
How much Lambda compute is consumed?
How much workflow state is stored?
How does performance change with concurrency?
What happens when multiple steps fail?
What happens when a failure lasts several minutes?
How does the system behave when a workflow is cancelled?
What is the operational cost of recovery?
These questions are more valuable than simply comparing average execution time.
Build a Representative Workflow
Avoid a benchmark containing five empty Lambda functions.
Each activity should perform realistic work.
For example:
Step 1: Insert order
Step 2: Reserve inventory
Step 3: Call payment service
Step 4: Create shipment record
Step 5: Publish notification
Each step should have measurable duration.
For example:
| Step | Typical Work |
|---|---|
| Create Order | PostgreSQL write |
| Reserve Inventory | Database transaction |
| Process Payment | External HTTP request |
| Create Shipment | Database write |
| Send Notification | Messaging/API call |
This creates a meaningful failure-recovery scenario.
Keep Business Semantics Identical
The conventional and durable implementations should perform the same logical operations.
If the traditional implementation performs:
Database
HTTP API
Database
Queue
the durable implementation should not replace those operations with mocks that execute instantly.
Otherwise, the benchmark measures different applications.
The workflow engine should be the primary variable.
Establish a Success Baseline
Before introducing failures, measure the successful path.
Run the workflow at:
1 concurrent workflow
10 concurrent workflows
50 concurrent workflows
100 concurrent workflows
500 concurrent workflows
The exact levels depend on the expected production load.
Measure:
End-to-end latency.
Lambda execution duration.
Activity duration.
Workflow completion rate.
Requests per second.
Database operations.
External API requests.
Memory usage.
Error rate.
This creates the baseline against which failure recovery can be compared.
Measure Recovery Latency Separately
Recovery latency is different from total workflow latency.
Suppose:
Workflow starts at 10:00:00
Payment fails at 10:00:03
Payment becomes available at 10:00:10
Workflow completes at 10:00:11
The recovery latency is approximately:
10:00:11 - 10:00:10 = 1 second
while the overall workflow duration is approximately 11 seconds.
These metrics answer different questions.
A durable workflow might have slightly higher successful-path overhead but significantly lower recovery cost after a late-stage failure.
Inject Controlled Failures
Failure injection is essential.
Do not wait for random production failures to determine whether the architecture recovers correctly.
Add controlled failure modes such as:
Fail step 1
Fail step 2
Fail step 3
Fail step 4
Fail step 5
Then repeat the same test multiple times.
For example:
Test A: Payment fails once
Test B: Payment fails twice
Test C: Payment unavailable for 30 seconds
Test D: Payment unavailable for 5 minutes
This exposes how the architecture behaves under different failure durations.
Compare Early and Late Failures
Failure position is an important benchmark dimension.
Consider:
Workflow A
Failure at step 1
Workflow B
Failure at step 3
Workflow C
Failure at step 5
A traditional retry becomes increasingly expensive as the failure moves later in the workflow because more work has already been completed.
If five steps take:
1s + 2s + 3s + 2s + 1s
and step 5 fails, restarting the workflow can repeat approximately nine seconds of previous work.
A durable workflow can potentially resume from the failed step.
This is one of the primary hypotheses the benchmark should test.
Measure Repeated Work
Create a metric for repeated business operations.
For example:
Expected database writes
Actual database writes
Expected HTTP requests
Actual HTTP requests
Suppose one workflow should produce:
5 database/API operations
but a failure causes:
8 operations
The additional three operations represent duplicated work.
A useful metric is:
Replay overhead =
(actual operations - expected operations)
/
expected operations
This gives a workload-specific measure of recovery efficiency.
Make Activities Idempotent
Durable workflows do not remove the need for idempotency.
An activity may be retried or replayed depending on the workflow implementation and failure mode.
For example:
public async Task ProcessPaymentAsync(
string orderId,
CancellationToken cancellationToken)
{
var existing = await paymentRepository
.FindByOrderIdAsync(orderId, cancellationToken);
if (existing is not null)
{
return;
}
await paymentGateway.ChargeAsync(
orderId,
cancellationToken);
}
The exact implementation should depend on the payment system.
The important principle is that repeating the same logical activity should not accidentally create duplicate business effects.
Separate Orchestration From Activity Work
A durable workflow should coordinate work rather than perform large amounts of business logic inside the orchestration layer.
A useful conceptual structure is:
Orchestrator
|
+--> Activity
|
+--> Activity
|
+--> Activity
The orchestrator determines what should happen.
Activities perform external work.
This separation makes benchmarking easier because orchestration overhead can be measured separately from actual business operations.
Benchmark State Size
Durable execution generally requires persistence of workflow state.
That state has a cost.
Test workflows with:
Small state
Medium state
Large state
For example:
| Workflow State | Example |
|---|---|
| Small | IDs and status |
| Medium | Order metadata |
| Large | Detailed processing context |
| Very large | Large serialized payload |
Avoid storing large response bodies or unnecessary objects in workflow state.
Instead, store references:
{
"orderId": "12345",
"paymentId": "pay-98765",
"status": "pending"
}
rather than embedding an entire external API response.
Smaller state generally makes workflow persistence and recovery easier to reason about.
Measure Orchestration Overhead
A successful durable workflow may take longer than a direct Lambda implementation because additional coordination is required.
That is not necessarily a problem.
Measure the difference.
For example:

Join the conversation! Your thoughts help the community grow.