Durable Functions
Durable Functions is an extension of Azure Functions that lets you write stateful, long-running workflows as code in a serverless environment. Normal functions are stateless and short-lived. Durable Functions adds orchestration on top: you write a function that calls other functions in sequence or in parallel, waits for events or timers, and survives restarts, scale-out, and crashes without losing its place.
Function types
- Client function: starts, queries, signals, or terminates orchestration instances (usually an HTTP, queue, or timer trigger).
- Orchestrator function: defines the workflow. It calls activities, awaits results, and decides what happens next.
- Activity function: a unit of work (call an API, write to a DB). This is where the actual I/O happens.
- Entity function (Durable Entities): a small piece of named, addressable state (a counter, a shopping cart) with operations you can signal or call.
How it works
Durable Functions uses event sourcing and replay. Each step the orchestrator takes is recorded in a history table in the configured storage backend. When an orchestrator awaits something (an activity result, a timer, an external event), it is unloaded from memory, so you pay for nothing while it waits. When the awaited thing completes, the orchestrator is reloaded and replays from the start, using the recorded history to skip work already done, until it reaches the next new step.
This is why orchestrator code has strict rules (see below).
Storage backends: Azure Storage (default), the Netherite backend (high throughput, uses Event Hubs and page blobs), and the Microsoft SQL backend (state in SQL Server or Azure SQL). The Durable Task Scheduler is a newer managed backend; check the current docs for availability and status.
Example (C# isolated worker): function chaining
public static class OrderWorkflow
{
[Function("OrderOrchestrator")]
public static async Task<string> RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var order = context.GetInput<Order>();
await context.CallActivityAsync("ReserveInventory", order);
await context.CallActivityAsync("ChargePayment", order);
await context.CallActivityAsync("SendConfirmation", order);
return $"Order {order.Id} completed";
}
[Function("ReserveInventory")]
public static Task ReserveInventory([ActivityTrigger] Order order, FunctionContext ctx)
{
// call inventory service
return Task.CompletedTask;
}
// ChargePayment and SendConfirmation follow the same pattern
}
public static class Starter
{
[Function("StartOrder")]
public static async Task<HttpResponseData> Start(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req,
[DurableClient] DurableTaskClient client)
{
var order = await req.ReadFromJsonAsync<Order>();
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
"OrderOrchestrator", order);
return await client.CreateCheckStatusResponseAsync(req, instanceId);
}
}
The starter returns an HTTP 202 with status-query URLs, so callers can poll for progress.
Common patterns
1. Function chaining: run steps in order, passing output to the next (above).
2. Fan-out/fan-in: run many activities in parallel and aggregate the results.
var tasks = files.Select(f => context.CallActivityAsync<int>("ProcessFile", f));
int[] results = await Task.WhenAll(tasks);
int total = results.Sum();
3. Async HTTP API: start a long operation and return a status endpoint instead of holding a connection open.
4. Monitor: poll something repeatedly until a condition is met, with flexible intervals (a durable timer loop instead of a fragile timer trigger).
5. Human interaction / approval: wait for an external event with a timeout.
using var cts = new CancellationTokenSource();
var approval = context.WaitForExternalEvent<bool>("Approval");
var timeout = context.CreateTimer(context.CurrentUtcDateTime.AddDays(3), cts.Token);
if (await Task.WhenAny(approval, timeout) == approval)
{
cts.Cancel(); // cancel the timer so the orchestration can complete
// approved or rejected
}
else
{
// escalate
}
6. Aggregator (stateful entities): accumulate state from many sources into one addressable entity.
7. Saga / compensation: if a later step fails, run compensating activities (refund payment, release inventory) in a catch block.
Built-in reliability features
- Automatic retries for activities with configurable backoff.
- Durable timers that can wait minutes, days, or longer at no compute cost.
- Sub-orchestrations to compose workflows from smaller ones.
- Checkpointing: if the host crashes mid-workflow, it resumes from the last completed step.
- Instance management APIs: query, suspend, resume, terminate, rewind, and purge instances.
Orchestrator code constraints
Because the orchestrator is replayed, its code must be deterministic:
- Don't use
DateTime.UtcNow; usecontext.CurrentUtcDateTime. - Don't use
Guid.NewGuid(); usecontext.NewGuid(). - Don't do I/O (HTTP, DB, file access) or read changing config in the orchestrator; do that in activities.
- Don't use
Task.DelayorThread.Sleep; usecontext.CreateTimer. - Don't use threads or non-durable async constructs; only await durable tasks.
- Use
context.CreateReplaySafeLogger(...)so logs aren't duplicated on every replay. - Keep inputs, outputs, and history small. Large payloads bloat storage; pass references (blob URLs, IDs) instead.
- Version carefully. Changing orchestrator logic while instances are in flight can break replay, so use versioning strategies or drain running instances first.
When to use Durable Functions
Good fits:
- Multi-step business processes that must complete reliably (order fulfillment, onboarding, provisioning)
- Long-running workflows lasting minutes to days or longer
- Human-in-the-loop approvals with timeouts and escalation
- Parallel batch processing with aggregation (process thousands of files, then summarize)
- Saga-style transactions across services that need compensation
- Polling and monitoring loops
- Replacing hand-rolled state machines built from queues and tables
Consider something else when:
- The work is a single short operation. A plain function is simpler.
- You need very high-throughput, ultra-low-latency processing. Orchestration adds storage round-trips, so look at plain queue triggers, Event Hubs, or the Netherite backend.
- You want a low-code, visual designer with many connectors. Logic Apps fits better.
- The workflow is complex enough to need a dedicated engine with rich versioning, search, and visibility. Temporal or a similar engine may fit, though it brings more operational work.
- You need strict message ordering, sessions, and broker features. Service Bus is the better primitive, possibly alongside Durable Functions.
Durable Functions vs. related options
| Durable Functions | Logic Apps | Service Bus + Functions | Plain Functions | |
|---|---|---|---|---|
| Style | Code-first workflows | Visual/low-code designer | Messaging between steps | Single stateless step |
| State | Managed for you | Managed for you | You manage it | None |
| Long waits, timers, approvals | Built in | Built in | Manual | Manual |
| Complex logic and loops | Natural, it's just code | Possible, harder to maintain | Scattered across handlers | N/A |
| Best for | Developers coding complex workflows | Integration with many SaaS connectors | Simple decoupled processing | Event handlers |
They combine well. For instance, a Service Bus-triggered function can start an orchestration, and an orchestration can call out to Logic Apps or other services.
Operations and cost
- Monitoring: Application Insights captures orchestration history and traces. Use the instance management APIs, Durable Functions Monitor, or the Durable Task Scheduler dashboard (where available) to inspect state.
- Cost: you pay for function execution plus storage transactions for state. Chatty orchestrations with many tiny activities can drive up storage costs, so batch work sensibly.
- Hosting: works on Consumption, Flex Consumption, Premium, and Dedicated plans. Check current docs for backend support on each plan.
- Housekeeping: purge completed instance history periodically so storage doesn't grow without bound.
Quick rule of thumb
- If your workflow is "do A, then B, then C, maybe wait for something, and don't lose my place if something crashes", use Durable Functions.
- If it is "react to one event and do one thing", use a normal function.
- If you want to click together connectors, use Logic Apps.