Azure Durable Functions
What is Durable Functions?
Durable Functions is an extension of Azure Functions that lets you write stateful workflows in a serverless environment.
The Problem It Solves
Regular Azure Functions are stateless - they run, finish, and forget everything. But real-world workflows often need to:
- Wait for external events (human approval, API callbacks)
- Chain multiple steps together reliably
- Run long-running processes (hours/days)
- Retry failed operations
- Fan out work in parallel, then aggregate results
How It Works
The extension automatically manages:
- ✅ State persistence (checkpointing)
- ✅ Automatic restarts after failures
- ✅ Replay of orchestration history
- ✅ Durable timers (wait for days without paying for idle compute)
Function Types
| Type | Role |
|---|---|
| Orchestrator | Defines the workflow logic - calls other functions, waits, branches |
| Activity | The actual work units - call APIs, process data, etc. |
| Entity | Stateful objects (like actors) for managing shared state |
| Client | Starts/queries/terminates orchestrations |
Common Patterns
1. Function Chaining
Execute a sequence of functions in a specific order: A → B → C → D
2. Fan-out/Fan-in
Run N tasks in parallel, wait for all to complete, then aggregate results.
3. Async HTTP APIs
Long-running operations with status polling endpoints.
4. Human Interaction
Wait for external approval/input with configurable timeout.
5. Aggregator (Stateful Entities)
Collect and aggregate data over time into a single entity.
6. Monitor
Recurring process that monitors a condition until met.
Code Examples
Basic Orchestration (C#)
[Function(nameof(MyOrchestrator))]
public static async Task<List<string>> MyOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var outputs = new List<string>();
// Chain activities - state survives failures
outputs.Add(await context.CallActivityAsync<string>("SayHello", "Tokyo"));
outputs.Add(await context.CallActivityAsync<string>("SayHello", "London"));
outputs.Add(await context.CallActivityAsync<string>("SayHello", "Seattle"));
return outputs;
}
[Function("SayHello")]
public static string SayHello([ActivityTrigger] string city)
{
return $"Hello, {city}!";
}
Fan-out/Fan-in Pattern
[Function(nameof(FanOutFanIn))]
public static async Task<int> FanOutFanIn(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var tasks = new List<Task<int>>();
// Fan out - start all tasks in parallel
for (int i = 0; i < 10; i++)
{
tasks.Add(context.CallActivityAsync<int>("ProcessItem", i));
}
// Fan in - wait for all to complete
int[] results = await Task.WhenAll(tasks);
return results.Sum();
}
Human Approval with Timeout
[Function(nameof(ApprovalWorkflow))]
public static async Task<string> ApprovalWorkflow(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
await context.CallActivityAsync("SendApprovalRequest", null);
using var cts = new CancellationTokenSource();
var timeout = context.CreateTimer(
context.CurrentUtcDateTime.AddHours(72), cts.Token);
var approval = context.WaitForExternalEvent<bool>("ApprovalEvent");
var winner = await Task.WhenAny(approval, timeout);
if (winner == approval)
{
cts.Cancel();
return approval.Result ? "Approved" : "Rejected";
}
return "Timed out";
}
Durable Entity (Counter)
[Function(nameof(Counter))]
public static void Counter([EntityTrigger] TaskEntityDispatcher dispatcher)
{
dispatcher.DispatchAsync<CounterEntity>();
}
public class CounterEntity
{
public int Value { get; set; }
public void Add(int amount) => Value += amount;
public void Reset() => Value = 0;
public int Get() => Value;
}
When to Use
✅ Use Durable Functions for:
- Multi-step workflows with dependencies
- Long-running processes (hours/days/weeks)
- Coordination between multiple services
- Stateful event processing
- Reliable retry logic
- Human-in-the-loop workflows
❌ Don't use for:
- Simple request/response APIs
- Real-time, low-latency requirements (< 100ms)
- When Logic Apps fits better (visual designer, 400+ connectors)
- Simple stateless operations
Hosting Options
| Plan | Best For |
|---|---|
| Consumption | Sporadic workloads, pay-per-execution |
| Flex Consumption | Variable load with fast scaling |
| Premium | Pre-warmed instances, VNet, longer timeouts |
| Dedicated | Predictable workloads, full control |
Storage Backends
| Provider | Description |
|---|---|
| Azure Storage | Default. Uses queues, tables, and blobs |
| Netherite | High-throughput, uses Event Hubs + Azure Storage |
| MSSQL | SQL Server/Azure SQL for state storage |
Best Practices
- Keep orchestrators deterministic - No I/O, random, or DateTime.Now
- Use activity functions for I/O - API calls, database operations
- Idempotent activities - Activities may be retried
- Version your orchestrations - Use deployment slots for zero-downtime
- Monitor with Application Insights - Built-in telemetry support
- Set appropriate timeouts - Avoid infinite waits
Latest Updates (2024-2025)
- Durable Task Scheduler - New dedicated SKU with predictable pricing
- Extension v3.x - All new features, upgrade from v2.x recommended
- .NET Isolated Worker - Full support with strongly-typed calls
- OpenTelemetry - Unified observability support