Long-running workflows are different from ordinary application requests.
A normal HTTP request might finish in a few milliseconds or seconds. A business workflow may need to run for minutes, hours, or even days while waiting for external systems, approvals, scheduled events, retries, or human interaction.
Trying to keep such workflows alive inside a normal web request creates reliability problems.
The application can restart. A container can be replaced. A network connection can fail. An external API can become unavailable. A process can crash halfway through an operation.
Durable execution addresses this problem by separating workflow state from the lifetime of the process executing it.
Instead of depending on one application instance staying alive, the workflow records its progress so it can continue after interruptions.
For .NET applications, this pattern is particularly useful for order processing, financial workflows, document processing, data pipelines, provisioning, and AI-assisted multi-step operations.
What Is Durable Execution?
Durable execution allows a workflow to preserve its state and continue from a known point after failures.
Consider an order-processing workflow:
Receive Order
↓
Validate Payment
↓
Reserve Inventory
↓
Create Shipment
↓
Send Confirmation
A traditional implementation may execute everything inside one process.
If the process crashes after inventory reservation:
Receive Order
↓
Validate Payment
↓
Reserve Inventory
↓
X Process Crash
The application needs to determine what happened before it can safely continue.
With durable execution, workflow progress becomes persistent:
Workflow State
↓
Step 1 Completed
↓
Step 2 Completed
↓
Step 3 Completed
After a restart, the workflow engine can resume from the appropriate point instead of treating the workflow as completely new.
Why Long-Running Workflows Are Difficult
Long-running workflows introduce several failure modes.
Process Failure
A worker can terminate unexpectedly.
Infrastructure Failure
A container, virtual machine, or server can disappear.
Network Failure
A downstream service may timeout or become temporarily unavailable.
Dependency Failure
A database, payment service, or third-party API may become unavailable.
Partial Completion
The most difficult situation is when the application does not know whether an external operation succeeded.
For example:
Application
↓
Payment Service
↓
Payment succeeds
↓
Network connection fails
The application receives a timeout.
Did the payment fail?
Or did the payment succeed but the response disappear?
Durable workflows need explicit strategies for handling these cases.
Durable Execution vs Background Jobs
Background jobs and durable workflows solve related but different problems.
A background job generally looks like:
Queue
↓
Worker
↓
Execute Job
This works well for independent tasks.
A durable workflow is more structured:
Workflow
↓
Step A
↓
Step B
↓
Wait
↓
Step C
↓
Retry
↓
Step D
The workflow needs to remember:
Which steps completed
Which step is currently executing
Which events have been received
Which retries have occurred
What data is required for the next step
When execution should resume
That state is the key distinction.
A Typical .NET Architecture
A durable workflow architecture can be organized into several components:
┌─────────────────┐
│ .NET API │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Workflow Engine │
└────────┬────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
Activity A Activity B Activity C
│ │ │
▼ ▼ ▼
Database API Queue
│
▼
┌─────────────────┐
│ Durable State │
└─────────────────┘
The API starts the workflow, while workers execute individual activities.
The durable state allows the workflow to survive worker failures.
Model Workflows as Explicit Steps
A good workflow should have clearly defined activities.
For example:
ProcessOrder
├── ValidateOrder
├── AuthorizePayment
├── ReserveInventory
├── CreateShipment
└── NotifyCustomer
This structure makes failures easier to reason about.
If ReserveInventory fails, the workflow knows which activity failed rather than having to infer the state of an arbitrary block of application code.
Keep Activities Small
A common mistake is putting too much work inside a single activity.
Avoid:
ProcessEverything()
Prefer:
ValidateOrder()
AuthorizePayment()
ReserveInventory()
CreateShipment()
SendNotification()
Smaller activities provide better observability and retry boundaries.
They also make recovery easier.
Retries Need Idempotency
Retries are one of the most important parts of durable execution.
Suppose:
AuthorizePayment()
times out.
The workflow retries the operation.
But the original request might already have succeeded.
You could accidentally charge the customer twice.
The solution is idempotency.
An operation is idempotent when executing it multiple times produces the same intended result as executing it once.
For example:
public async Task<PaymentResult> AuthorizePayment(
PaymentRequest request,
CancellationToken cancellationToken)
{
var existing = await paymentStore
.FindByIdempotencyKeyAsync(
request.IdempotencyKey,
cancellationToken);
if (existing is not null)
{
return existing;
}
var result = await paymentGateway.AuthorizeAsync(
request,
cancellationToken);
await paymentStore.SaveAsync(
request.IdempotencyKey,
result,
cancellationToken);
return result;
}
The idempotency key allows a retry to recognize that the operation has already been processed.
Design Every External Activity for Failure
External calls should never be treated as guaranteed.
Consider:
var response = await httpClient.PostAsJsonAsync(
"/inventory/reserve",
request,
cancellationToken);
The call can fail because of:
Timeout
DNS failure
Connection reset
HTTP 5xx response
Rate limiting
Authentication problems
Service outage
A durable workflow should classify failures instead of retrying everything.
Not Every Error Should Be Retried
Transient failures are usually good retry candidates:
HTTP 408
HTTP 429
HTTP 500
HTTP 502
HTTP 503
HTTP 504
Permanent failures generally require different handling:
Invalid request
Invalid customer
Insufficient inventory
Invalid authentication
Business rule violation
Blind retries can turn a simple failure into a larger outage.
Use Exponential Backoff
A workflow should generally avoid retrying immediately in a tight loop.
Instead:
Join the conversation! Your thoughts help the community grow.