This chapter covers Durable Functions, an extension of Azure Functions that enables stateful workflows in a serverless environment. You will learn three core patterns: function chaining (sequential execution), fan-out/fan-in (parallel execution), and human interaction (asynchronous approval workflows). These patterns are heavily tested on the AZ-204 exam, appearing in approximately 15–20% of questions related to compute and serverless. Understanding the internal mechanisms, default timeouts, and storage dependencies is critical for both the exam and real-world implementation.
Jump to a section
Imagine a car assembly line. The line has a main conveyor belt (the orchestrator function). When a car chassis arrives, the conveyor belt moves it to a workstation where the engine is installed (function chaining). But sometimes, you need to install all four wheels simultaneously — that's fan-out. The conveyor belt stops, and four robotic arms (activity functions) each install one wheel in parallel. The conveyor belt waits until all four arms report completion before moving on. Now consider human interaction: if a part is missing, the conveyor belt pauses and sends a notification to a human supervisor. The supervisor has a limited time (e.g., 30 minutes) to respond. If they respond with 'use substitute part', the line continues; if they don't respond, the line escalates to a manager. The conveyor belt itself never installs parts — it only orchestrates the workflow. The robotic arms are stateless and can be scaled independently. The conveyor belt maintains state (which chassis is where, which steps completed) in a durable storage — in Durable Functions, this is the Azure Storage queue and table. The belt's position is like the orchestration history — it can be replayed from any point if the belt crashes. This exactly mirrors Durable Functions: orchestrators coordinate, activities do work, fan-out/fan-in runs parallel tasks, and human interaction uses durable timers and external events.
What Are Durable Functions and Why Do They Exist?
Durable Functions is an extension of Azure Functions that allows you to write stateful functions in a serverless compute environment. The core problem it solves is that standard Azure Functions are stateless — they execute in response to an event and then terminate, with no built-in mechanism to maintain state across multiple invocations or to orchestrate complex workflows that involve waiting, retries, and parallel execution. Durable Functions addresses this by introducing a durable orchestration framework that automatically manages state, checkpoints, and replays.
How It Works Internally — The Orchestrator and Activity Functions
Durable Functions introduces two main function types: orchestrator functions and activity functions. Orchestrator functions define the workflow using procedural code (e.g., C# async methods) and coordinate the execution of activity functions. Activity functions are the actual units of work — they perform one specific action, such as calling an external API, writing to a database, or processing a file.
The orchestrator function is deterministic: every time it runs, it must produce the same sequence of calls to activity functions given the same input. This determinism is enforced by the Durable Functions runtime through a mechanism called replay. The orchestrator function's execution history is persisted in Azure Storage (specifically in a table named DurableFunctionsHistory). If the orchestrator is interrupted (e.g., due to a host restart), it can be replayed from the beginning by reading the history and skipping already-completed steps. This ensures that the orchestrator is resilient to failures without requiring explicit checkpointing code.
Key Components, Values, Defaults, and Timers
Task Hubs: A logical container for Durable Functions resources within a storage account. Each task hub uses separate queues, tables, and containers. Default name is the function app name.
Storage Account: Required. Durable Functions uses Azure Storage queues for message delivery, tables for orchestration history, and blobs for leases and large messages.
Control Queue: A queue that holds messages for orchestrator functions. Each task hub has one or more control queues. Default number of partitions is 4, configurable via maxConcurrentTaskOrchestrationWorkItems.
Work-Item Queue: A queue for activity functions. Each task hub has one work-item queue.
Orchestration Instance ID: A unique identifier for each orchestration execution. You can provide it when starting an orchestration, or let the system auto-generate a GUID.
Durable Timer: Created using CreateTimer in the orchestrator. Timers are durable — they survive host restarts. The minimum timer resolution is 1 second, but practical minimum is about 1 minute due to storage latency.
Retry Policies: Activity functions can be configured with automatic retry policies. Default: no retries. You can specify MaxNumberOfAttempts, FirstRetryInterval (default 1 second), BackoffCoefficient (default 2), and MaxRetryInterval.
Human Interaction Timeout: Typically implemented using a durable timer that waits for an external event. The external event is sent via RaiseEventAsync. Common timeout values: 24 hours, 7 days. The maximum timer duration is limited by the storage account's queue message TTL (7 days by default, configurable up to infinite).
Configuration and Verification Commands
To enable Durable Functions in a function app, you must add the DurableTask extension. In the host.json file, you can configure the storage connection string and task hub name:
{
"version": "2.0",
"extensions": {
"durableTask": {
"hubName": "MyTaskHub",
"storageProvider": {
"connectionStringName": "DurableFunctionsStorage",
"partitionCount": 4
}
}
}
}To verify correct configuration, you can monitor the storage account: check that queues (e.g., MyTaskHub-control-00, MyTaskHub-workitems) and tables (e.g., DurableFunctionsHistory, DurableFunctionsInstances) are created. Use Azure Storage Explorer or CLI:
az storage table list --connection-string "<connection-string>" --query "[?contains(name, 'DurableFunctions')]"How Durable Functions Interacts with Related Technologies
Azure Functions: Durable Functions is built on top of Azure Functions. All triggers and bindings available in Azure Functions are also available in activity functions, but orchestrator functions have restrictions: they cannot use I/O directly (e.g., HTTP calls, database queries) — they must use activity functions for any I/O.
Azure Storage: The default storage provider. For production, consider using the Netherite provider (which uses Event Hubs) or the Microsoft SQL Server provider for better performance and scalability.
Azure Logic Apps: Both can orchestrate workflows, but Durable Functions is code-first, more flexible, and cheaper for high-volume scenarios. Logic Apps is designer-driven and better for simple integrations.
Azure Event Grid: Can be used to trigger orchestrations or receive notifications (e.g., when a human interaction step completes).
Pattern: Function Chaining
Function chaining executes a sequence of activities in a specific order. The orchestrator calls each activity one after another, and the output of one can be passed as input to the next. This is implemented using await in C#:
[FunctionName("Chaining")]
public static async Task<List<string>> RunOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var input = context.GetInput<string>();
var result1 = await context.CallActivityAsync<string>("Activity1", input);
var result2 = await context.CallActivityAsync<string>("Activity2", result1);
var result3 = await context.CallActivityAsync<string>("Activity3", result2);
return new List<string> { result1, result2, result3 };
}The orchestrator replays after each activity completes. If the host crashes after Activity1 but before Activity2, the orchestrator replays from the start, sees that Activity1 has already completed (via history), and skips directly to calling Activity2.
Pattern: Fan-Out/Fan-In
Fan-out/fan-in executes multiple activities in parallel and then aggregates their results. This is done using Task.WhenAll:
[FunctionName("FanOutFanIn")]
public static async Task<int> RunOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var workBatch = await context.CallActivityAsync<string[]>("GetWorkBatch", null);
var tasks = new Task<int>[workBatch.Length];
for (int i = 0; i < workBatch.Length; i++)
{
tasks[i] = context.CallActivityAsync<int>("ProcessItem", workBatch[i]);
}
var results = await Task.WhenAll(tasks);
var total = results.Sum();
return total;
}Key points: The orchestrator issues all activity calls immediately (fan-out) and then awaits all of them (fan-in). The number of concurrent activities is limited by the maxConcurrentTaskActivityWorkItems setting (default 10 per host). For large fan-outs, consider batching or using a sub-orchestrator pattern.
Pattern: Human Interaction
Human interaction involves waiting for an external event (e.g., approval) with a timeout. The orchestrator creates a durable timer and waits for either the timer to expire or an external event to be raised:
[FunctionName("ApprovalWorkflow")]
public static async Task RunOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
await context.CallActivityAsync("RequestApproval", null);
using (var timeoutCts = new CancellationTokenSource())
{
DateTime deadline = context.CurrentUtcDateTime.AddHours(24);
Task timeoutTask = context.CreateTimer(deadline, timeoutCts.Token);
Task approvalTask = context.WaitForExternalEvent("ApprovalResult");
var winner = await Task.WhenAny(timeoutTask, approvalTask);
if (winner == approvalTask)
{
// Approval received
}
else
{
// Timeout occurred
await context.CallActivityAsync("Escalate", null);
}
}
}The external event is raised from another function (e.g., an HTTP-triggered function) using:
await client.RaiseEventAsync(instanceId, "ApprovalResult", approvalValue);Important: The WaitForExternalEvent call is durable — if the host restarts while waiting, it will resume waiting after replay. The timer is also durable.
Error Handling and Retries
Activity functions can throw exceptions. The orchestrator can catch them and implement retry logic using CallActivityWithRetryAsync:
try
{
await context.CallActivityWithRetryAsync("FlakyActivity",
new RetryOptions(TimeSpan.FromSeconds(5), 3)
{
BackoffCoefficient = 2.0,
MaxRetryInterval = TimeSpan.FromMinutes(1)
}, input);
}
catch (Exception ex)
{
// Handle after retries exhausted
}Performance and Scaling
Throughput: Durable Functions can handle thousands of orchestrations per second, but performance depends on the storage provider. The default Azure Storage provider is suitable for most workloads; Netherite can achieve 10x higher throughput.
Scale: The function app scales out based on the number of messages in the control queues. Each host processes one message at a time per control queue partition. Increase partition count for higher concurrency.
Latency: Orchestrator replay adds overhead. For very low-latency requirements, consider minimizing the number of activity calls or using the Netherite provider.
Common Pitfalls
Non-deterministic code: Using DateTime.Now, Guid.NewGuid(), or random numbers directly in orchestrator functions will cause replay errors. Use context.CurrentUtcDateTime and context.NewGuid() instead.
Infinite loops: Orchestrators must terminate. Ensure that loops have proper exit conditions.
Large payloads: Passing large inputs/outputs between activities can cause storage pressure. Use external storage references (e.g., blob URLs) for large data.
Timer accuracy: Durable timers are not real-time; they are checked periodically. The default polling interval is 1 second, but actual resolution may be coarser under load.
Define the Orchestrator Function
Create an orchestrator function using the `[OrchestrationTrigger]` binding. This function defines the workflow using procedural code. It must be deterministic — no direct I/O, no random values, no DateTime.Now. Use `context.CurrentUtcDateTime` for time and `context.NewGuid()` for unique IDs. The orchestrator is replayed on every execution, so any side effects must be idempotent. The function should be a static method returning `Task` (C#) or equivalent in other languages.
Implement Activity Functions
Activity functions are stateless functions triggered by the `[ActivityTrigger]` binding. They perform the actual work — calling APIs, processing data, etc. Each activity function should do one thing and do it well. They can be scaled independently. Activity functions can use any Azure Functions binding (HTTP, Blob, etc.). They receive input from the orchestrator and return output. They can throw exceptions, which the orchestrator can catch and retry.
Configure Storage and Task Hub
Durable Functions requires an Azure Storage account. In `host.json`, configure the `durableTask` section with a `hubName` and `storageProvider` connection string. The task hub name must be unique within a storage account. The storage account must have queues, tables, and blobs enabled. For production, use a separate storage account for Durable Functions to avoid throttling from other workloads. Default partition count is 4; increase for higher concurrency.
Start the Orchestration
Use a client function (e.g., HTTP-triggered) to start the orchestration. Call `starter.StartNewAsync("OrchestratorFunction", input)` where `starter` is an `IDurableOrchestrationClient`. This returns an instance ID. The client function can return the instance ID to the caller for status checks. Orchestrations can be started from various triggers: HTTP, Queue, Timer, etc. The start message is placed in the control queue.
Monitor and Manage Instances
Use the `IDurableOrchestrationClient` to query status, raise events, terminate, or purge instances. The `GetStatusAsync` method returns orchestration status (Running, Completed, Failed, etc.). Use `RaiseEventAsync` to send external events. Use `TerminateAsync` to stop a running orchestration. Use `PurgeInstanceHistoryAsync` to clean up completed instances (important for cost and performance). The status is stored in the `DurableFunctionsInstances` table.
Enterprise Scenario 1: Order Processing Pipeline
A large e-commerce company uses Durable Functions to process orders from submission to fulfillment. The orchestration chains activities: validate payment, check inventory, reserve stock, ship, and send confirmation. Fan-out is used to check inventory across multiple warehouses in parallel. Human interaction is required when a payment fails — an email is sent to the customer with a link to update payment, and if not completed within 48 hours, the order is cancelled and inventory is released. In production, this system handles 10,000 orders per hour. The key configuration is setting the maxConcurrentTaskActivityWorkItems to 50 per host to balance throughput and resource usage. A common misconfiguration is using too few control queue partitions (default 4), which limits parallelism — increasing to 16 improved throughput by 3x.
Enterprise Scenario 2: Document Approval Workflow
A financial services firm uses Durable Functions for document approval. When a document is uploaded, an orchestrator starts: first, it runs an activity to classify the document, then fans out to multiple reviewers (each reviewer is an activity that sends an email with an approval link). The orchestrator waits for all reviewers to approve or reject. If any reviewer rejects, the entire workflow is terminated. A durable timer enforces a 7-day deadline; after that, the document is escalated to a manager. The system processes 500 documents per day. The main challenge is managing the large number of concurrent timers — each timer creates a storage queue message. The team had to increase the storage account's queue capacity and use the Netherite provider for better timer performance.
Enterprise Scenario 3: Data Pipeline with Fan-Out
A media company processes video uploads: transcode to multiple formats (fan-out), generate thumbnails, and extract metadata. Each activity runs in parallel. After all complete, the orchestrator aggregates results and updates a database. The fan-out can involve 10–20 parallel activities. The orchestrator uses CallActivityWithRetryAsync for each transcode activity with 3 retries and exponential backoff. In production, they noticed that if one activity fails and exhausts retries, the entire orchestration fails. They implemented a custom error-handling pattern: catch exceptions and store partial results, then continue. This required careful design of the orchestrator's state management.
Exactly What AZ-204 Tests on This Topic
AZ-204 objective 1.1 covers "Develop Azure compute solutions" and includes Durable Functions patterns. Specifically, you must know:
- Function chaining: How to sequence activities and pass data between them.
- Fan-out/fan-in: How to execute parallel tasks and aggregate results.
- Human interaction: How to implement durable timers and external events.
- Orchestrator constraints: Determinism, no I/O, use of CurrentUtcDateTime and NewGuid.
- Storage dependencies: Azure Storage requirements and configuration.
Common Wrong Answers and Why Candidates Choose Them
Using `DateTime.Now` in orchestrator: Candidates think it's fine because it's C#. Wrong — the orchestrator replays, so DateTime.Now would return different values each replay, causing non-determinism. Use context.CurrentUtcDateTime.
Using `HttpClient` directly in orchestrator: Candidates think they can make HTTP calls. Wrong — orchestrators must not perform I/O. Use an activity function to make the HTTP call.
Believing fan-out/fan-in requires `Task.WhenAll` with a list of tasks: While correct, many think they must use Task.WhenAll only. Actually, you can use any pattern that awaits multiple tasks. The key is that the orchestrator issues all calls before awaiting.
Thinking human interaction requires polling: Candidates propose a polling loop in the orchestrator. Wrong — use WaitForExternalEvent with a durable timer for efficiency and reliability.
Specific Numbers and Terms That Appear on the Exam
Default partition count for control queues: 4
Default retry policy: FirstRetryInterval = 1 second, BackoffCoefficient = 2, MaxRetryInterval = unlimited, MaxNumberOfAttempts = no limit (but you must specify).
Maximum timer duration: Limited by storage queue message TTL (default 7 days, configurable).
Task hub name: Must be unique within a storage account.
Instance ID: Can be provided or auto-generated (GUID).
Edge Cases and Exceptions the Exam Loves to Test
Orchestrator replay and non-deterministic code: The exam will present code using Random or DateTime.Now and ask if it's valid.
Large fan-out: The exam may ask about limits — there's no hard limit, but performance degrades. Use batching.
Timer accuracy: Durable timers are not real-time; they have a resolution of about 1 second but can be delayed under load.
Storage provider: The default is Azure Storage; Netherite and MSSQL are alternatives. The exam may ask which provider to use for higher throughput.
How to Eliminate Wrong Answers Using the Underlying Mechanism
If an answer suggests using Thread.Sleep in an orchestrator, eliminate it — orchestrators must not block threads.
If an answer suggests using context.CallActivity inside a Parallel.ForEach, consider that the orchestrator must await all tasks — Task.WhenAll is preferred.
If an answer suggests using context.CallActivity for a long-running operation, remember that activity functions have a default timeout of 5 minutes (configurable). For longer operations, use a sub-orchestrator.
Durable Functions enables stateful workflows in serverless Azure Functions using orchestrator and activity functions.
Orchestrator functions must be deterministic; use context.CurrentUtcDateTime and context.NewGuid() instead of DateTime.Now and Guid.NewGuid().
Function chaining sequences activities; fan-out/fan-in runs activities in parallel and aggregates results.
Human interaction uses durable timers and WaitForExternalEvent for approval workflows with timeouts.
Default storage provider is Azure Storage; configure task hub name and partition count in host.json.
Activity functions have a default timeout of 5 minutes (configurable up to 10 minutes on Consumption plan).
Retry policies can be set per activity call with FirstRetryInterval (default 1s), BackoffCoefficient (default 2), and MaxNumberOfAttempts.
Orchestrator replay reads history from storage; this ensures fault tolerance but adds latency.
Fan-out concurrency is limited by maxConcurrentTaskActivityWorkItems (default 10 per host) and control queue partition count (default 4).
Durable timers are persisted; their maximum duration is limited by storage queue message TTL (default 7 days).
External events are raised via RaiseEventAsync from a client function; the orchestrator waits using WaitForExternalEvent.
Common exam trap: using non-deterministic code in orchestrator, or using direct I/O instead of activity functions.
These come up on the exam all the time. Here's how to tell them apart.
Durable Functions
Code-first workflow definition (C#, JavaScript, Python, etc.)
Supports fan-out/fan-in, chaining, human interaction natively
Lower cost per execution for high-volume scenarios
Tight integration with Azure Functions ecosystem
Requires storage account for state management
Azure Logic Apps
Designer-driven, no-code/low-code workflow
Built-in connectors for 200+ services (SAP, Salesforce, etc.)
Higher per-execution cost, but easier to set up for simple workflows
Limited custom code (can use Azure Functions as a connector)
State managed internally by Logic Apps runtime
Mistake
Durable Functions requires a premium plan.
Correct
Durable Functions works on Consumption, Premium, and Dedicated plans. Consumption plan has a 10-minute timeout for all functions, but orchestrator replay resets the clock. Activity functions have a 5-minute default timeout (configurable up to 10 minutes on Consumption).
Mistake
Orchestrator functions can use any C# code.
Correct
Orchestrator functions must be deterministic. They cannot use `DateTime.Now`, `Guid.NewGuid()`, `Random`, or perform I/O (HTTP, database). They must use `context.CurrentUtcDateTime` and `context.NewGuid()`. Any non-deterministic code will cause replay errors.
Mistake
Fan-out/fan-in runs activities in parallel within the same process.
Correct
Activities are dispatched as separate function invocations. They can run on different hosts, even in different regions. The orchestrator only coordinates via storage messages.
Mistake
Human interaction requires a separate Azure Logic App.
Correct
Human interaction can be implemented entirely within Durable Functions using `WaitForExternalEvent` and `CreateTimer`. No external service is required, though you may use Logic Apps for complex approvals.
Mistake
Durable Functions state is stored in memory.
Correct
State is persisted in Azure Storage (or another provider). Orchestrator history is stored in tables, messages in queues, and large state in blobs. This ensures durability across restarts.
Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.
An orchestrator function defines the workflow and coordinates activities. It must be deterministic and cannot perform I/O. An activity function performs the actual work (e.g., calling an API, processing data) and can use any Azure Functions binding. The orchestrator calls activity functions via `context.CallActivityAsync`. Activity functions are stateless and can be scaled independently.
Use `context.CreateTimer` to create a durable timer that fires at the deadline, and `context.WaitForExternalEvent` to wait for an external event. Use `Task.WhenAny` to race the timer and the event. If the timer completes first, handle the timeout (e.g., escalate). The timer and event are both durable — they survive host restarts.
Yes. Durable Functions works on Consumption, Premium, and Dedicated plans. On Consumption, the default function timeout is 10 minutes, but orchestrator functions are not subject to this timeout because they replay. Activity functions have a default timeout of 5 minutes (configurable up to 10 minutes on Consumption).
Avoid passing large payloads directly as inputs/outputs because they are stored in Azure Tables (max 64 KB per entity). Instead, store large data in Azure Blob Storage and pass the blob URL. The activity can then read the blob. This is more efficient and avoids storage limits.
The exception is caught by the orchestrator. You can use `CallActivityWithRetryAsync` to automatically retry based on a retry policy. If retries are exhausted or you don't use retry, the exception propagates to the orchestrator, which can catch it and handle it (e.g., call a compensation activity). If unhandled, the orchestration fails.
Use the `IDurableOrchestrationClient` to query status by instance ID. The `GetStatusAsync` method returns the orchestration status (Running, Completed, Failed, etc.), custom status, and output. You can also enable Application Insights for detailed telemetry. The status is stored in the `DurableFunctionsInstances` table.
A task hub is a logical container that groups related Durable Functions resources within a storage account. Each task hub has its own queues, tables, and blobs. Multiple function apps can share the same storage account if they use different task hub names. The task hub name is configured in host.json under `durableTask.hubName`.
You've just covered Durable Functions: Fan-Out, Chaining, Human Interaction — now see how well it sticks with free AZ-204 practice questions. Full explanations included, no account needed.
Done with this chapter?