Azure Durable Functions: Understanding Basics
Back in 2022, I gave a presentation called Making the Best Out of Azure Durable Functions.
It has been a while since I worked with Durable Functions day to day, but the core ideas from that presentation are still just as important. Durable Functions looks simple when we first start using it, yet it behaves differently from a normal Azure Function in ways that are easy to miss.
Recently, my current client team came across that old presentation and thought it was quite interesting. They were in the process of migrating some older WebJob-style workflows into Azure Functions and asked me to put together a quick Durable Functions guide for the team.
I thought it was a great opportunity to revisit the topic, refresh my original guidance, and turn that request into my first blog series for the year.
This first post establishes the foundation: why we need Durable Functions, how the different function types work together, and how durable execution changes the way we design code. The next two posts will build on that foundation with design patterns and production best practices.
Navigate the Durable Functions Series
- Part 1: Understanding Basics [👈 You are here]
-
Part 2: Azure Durable Functions Design Patterns
-
Part 3: Azure Durable Functions Best Practices
Why Durable Functions?
Azure Functions is naturally stateless. A function receives a trigger, performs some work, and finishes. That model is excellent for a single operation such as processing a queue message, resizing an image, or responding to an HTTP request.
Serverless does not mean there are no servers. It means the servers are managed by the cloud platform rather than by us. The underlying worker can be restarted, moved, scaled in, or interrupted for maintenance and patching. That is normal cloud behaviour and, for a short stateless function, it is usually not a problem. For a long-running process, however, keeping progress only in memory means an infrastructure interruption could force the process to start again or leave us to work out where it stopped. Durable state moves that progress outside the lifetime of one worker, allowing the workflow to recover and continue on another worker when required.
The design becomes even more complicated when one business process needs several functions to work together.
Imagine an order workflow that needs to:
- Validate the order.
- Reserve stock.
- Process payment.
- Wait for a fulfilment response from an external partner.
- Track delivery.
- Compensate for completed steps if something later fails.
We can build that using normal queue-triggered Functions or WebJobs, but then we need to own the workflow state, correlation IDs, checkpoints, retries, timeouts, duplicate messages, error recovery, and status queries. Before long, the coordination code becomes its own workflow engine.
Durable Functions provides that coordination layer for us. It is an extension of Azure Functions that allows us to describe a stateful workflow in code while the Durable Task runtime takes responsibility for persisting progress and resuming the workflow when more work can continue.
This gives us several useful capabilities:
- Checkpoints and Recovery: completed steps are recorded, so the workflow can continue after the host restarts or scales.
- Long-Running Workflows: an orchestration can wait for hours, days, or months without keeping a thread or worker busy for the entire wait.
-
Durable Timers: the workflow can wait until a future time without using
Thread.Sleepor keeping compute active. - External Events: an orchestration can wait for a human approval, partner callback, or another business event.
- Retries and Compensation: transient failures and multi-system recovery can be expressed as part of the workflow.
- Parallel Execution: independent activities can fan out across workers and later be combined.
- Instance Management: callers and operators can query status, raise events, suspend, resume, terminate, and purge workflow instances.
- Durable Entities: small pieces of state can be addressed and updated through serialised operations.
The valuable part is not simply that a workflow can run for a long time. It is that the workflow can stop, recover, scale, and continue without us building all of that plumbing ourselves.
When Is It a Good Fit?
Durable Functions is a strong fit when the process is stateful, contains multiple steps, waits for something external, or needs reliable recovery across failures.
Typical examples include:
- Order processing and fulfilment.
- Customer onboarding.
- Approval and human-interaction workflows.
- Data-processing pipelines with parallel stages.
- Provisioning cloud resources across several systems.
- Scheduled monitoring and polling workflows.
- Multi-system processes that need compensating actions.
- Recurring background processes with durable state.
It is probably unnecessary for a single fire-and-forget operation, a short synchronous API call, or work that is already handled well by one queue-triggered function. If the requirement is mainly a visual workflow with a large catalogue of connectors, Logic Apps may be the better fit. If all the work occurs inside one database transaction, use the database transaction rather than inventing a distributed saga.
The question I ask is:
Am I coordinating a stateful business process, or am I simply running one background task?
How the Function Types Work Together
The most important design decision is deciding which responsibility belongs in which function. A Durable Functions application normally begins with three core roles: the starter, the orchestrator, and one or more activities.
flowchart LR
Trigger[Trigger] --> Starter[Starter / client]
Starter -->|Starts an instance| Orchestrator[Orchestrator]
Starter -->|Returns instance ID| Tracking[Tracking and management]
Orchestrator -->|Schedules work| Activity[Activity]
Activity -->|Returns a result| Orchestrator
Orchestrator --> Outcome[Completed or failed]
This is the core end-to-end flow. The starter accepts the trigger and starts an instance. The orchestrator coordinates the workflow, while activities perform the actual work, including calling databases, APIs, or other systems when required. Each activity result returns to the orchestrator so it can decide what happens next. External events and human approvals are additional patterns that I will cover in Part 2.
| Function type | Its job | A simple example | What should not live there |
|---|---|---|---|
| Starter or client | Act as a stateless entry point: receive and validate a trigger, use the Durable client to start or manage an orchestration, and return its instance ID or tracking response | Accept an order request, start OrderOrchestrator with the order ID, and return the instance details |
Long-running work, workflow coordination, or large payload preparation |
| Orchestrator | Describe the workflow by coordinating activities, sub-orchestrations, timers, external events, branches, retries, and compensation | Validate, reserve stock, take payment, wait for fulfilment, and arrange delivery | Direct I/O, database calls, HTTP calls, blocking work, CPU-heavy work, or non-deterministic logic |
| Activity | Perform a meaningful unit of I/O or CPU work and return a result | Reserve stock or call the payment provider | Coordinating other activities or hiding an entire workflow inside one function |
| Entity | Optionally hold and serialise access to a small piece of durable state | Maintain a counter, lock, or per-customer workflow state | Large document storage, unbounded state, or general database replacement |
The Starter Kicks Off
The starter is stateless. In most ways, it is just like any other Azure Function we already know: it can use an HTTP, queue, timer, Service Bus, or another supported trigger. The important difference is that it has access to a Durable client, which it uses to start and manage durable orchestration instances. This is different from the orchestration context used inside the orchestrator itself.
Its main job should remain simple:
- Receive and validate the request.
- Start the appropriate orchestration with a small input.
- Return the orchestration instance ID or a tracking response.
For an HTTP-triggered workflow, this will often be an HTTP 202 Accepted response containing the instance ID and URLs the caller can use to check status or perform supported management operations. The same Durable client can also query an instance, raise an external event, suspend or resume it, or terminate it when those operations are exposed through a suitable client function.
The starter should not execute or coordinate the workflow itself. Once the instance has been created and the tracking information returned, its job is normally done.
The Orchestrator Describes the Workflow
The orchestrator is the workflow definition. It decides what happens first, what can happen in parallel, what needs to wait, and what should happen after a failure.
It is useful to think of the orchestrator as the conductor of an orchestra. The conductor decides when each section plays but does not leave the podium to play every instrument.
The orchestrator therefore coordinates work but does not perform external work directly. It can schedule an activity or call a sub-orchestration. A sub-orchestration is simply another orchestrator function with its own workflow logic, giving us a clean way to separate, reuse, and manage a meaningful part of a larger process.
When an activity or sub-orchestration may take time, the orchestrator does not keep a thread blocked while it waits. It reaches a durable wait point, saves its progress, and allows the runtime to unload it. This is not Thread.Sleep or Task.Delay; no worker needs to sit idle waiting for the result.
When the activity or sub-orchestration finishes, its result wakes the parent orchestration. The runtime rebuilds the orchestrator’s state from its saved history and continues from the logical point where it left off. This rebuilding behaviour is called replay, which I will explain shortly.
Activities Perform the Work
Activities are where database calls, HTTP requests, file operations, CPU-heavy processing, and other side effects belong. They behave much more like normal Azure Functions and can use dependency injection and application services.
I prefer activities to be thin adapters over testable application services. The activity deals with the Durable Functions boundary; the service deals with the business operation. This keeps the function easy to understand and lets most business behaviour be tested without starting the Functions host.
There is no prize for creating hundreds of tiny activities either. An activity should represent a meaningful, independently retryable unit of work. The right boundary is often the point at which you can confidently answer:
Question to ask: If this activity runs twice with the same input, will it create a duplicate side effect?
How Durable Execution Works
The starter, orchestrator, and activities give us the visible structure of a Durable Functions application. The Task Hub, replay, and durable messaging explain how that structure survives restarts and scale events. They also explain why Durable Functions code comes with a few rules that normal function code does not have.
The Task Hub
A Task Hub represents the durable state of the application. It stores orchestration and entity instance state together with the pending messages that drive the next piece of work. When an orchestrator wakes up, the runtime uses its stored history to rebuild its local state and continue from the correct point.
With the default Azure Storage provider, that state is represented using queues, tables, and blobs. Other providers represent it differently, but the programming model is the same.
Two consequences explain almost every rule in this series:
The orchestrator replays: It may run through its code from the beginning many times, using the events already recorded in the Task Hub to rebuild its state up to the last known point.
Work is delivered through durable messages: Activities are decoupled from the orchestrator and may be delivered more than once in failure scenarios.
Replay rebuilds the orchestration state; it does not automatically call every completed activity again. I will explain this important distinction in the dedicated replay section below.
This is also one reason exceptions should be allowed to bubble up when the workflow cannot recover. The runtime can only record and expose the correct failed state when the failure is visible to it. I will return to exception handling in Part 3.
This is why orchestrators must be deterministic, activities must be idempotent, payload size matters, and logging needs special treatment.
The mental model to remember: The orchestrator’s state lives in durable storage and is rebuilt through replay. Understand that, and most Durable Functions rules stop feeling arbitrary.
Determinism Is Non-Negotiable
An orchestrator can run from the top each time new history arrives. During replay, the runtime matches durable operations in the code with events already recorded in history. For that matching to work, the code must make the same decisions in the same order when given the same history.
Inside an orchestrator, avoid:
- Current time APIs such as
DateTime.NoworDateTime.UtcNow. - Random values and normal GUID generation.
- Direct HTTP, database, file system, or network calls.
- Reading environment variables or configuration that could change between replays.
- Mutable static state.
-
Task.Delay, thread sleeps, locks, and non-durable asynchronous work. - CPU-heavy processing that holds the orchestration dispatcher.
Use the deterministic APIs exposed by the orchestration context for time, identifiers, durable timers, activities, sub-orchestrations, entities, and external events. Move everything else into an activity.
For example, inside a .NET isolated-worker orchestrator, use the context-provided time and a durable timer instead of DateTime.UtcNow and Task.Delay:
DateTime currentTime = context.CurrentUtcDateTime;
DateTime wakeUpAt = currentTime.AddMinutes(10);
await context.CreateTimer(wakeUpAt, CancellationToken.None);
CurrentUtcDateTime returns a replay-safe orchestration time. CreateTimer records the timer in durable history and allows the orchestrator to be unloaded until the deadline, rather than holding a thread asleep for ten minutes.
Determinism also applies to deployment. Adding, removing, or reordering an activity call can break an instance that started under the old code and later replays under the new code. Changing an activity name or changing the shape of a persisted input or output can have the same result.
The answer is not to stop changing orchestrations; workflow requirements will inevitably evolve. Instead, treat a change to a live workflow as a versioning decision so existing instances retain a compatible execution path. I will cover the available versioning approaches in Part 3.
This is the part many teams miss: an orchestrator can be perfectly deterministic within one release and still become non-deterministic across a deployment.
Replay Is Not the Same as Re-Execution
This distinction is worth making because it is easy to explain incorrectly.
When an orchestrator replays, a completed activity is normally not executed again. The runtime finds the activity result in history and gives that recorded result back to the orchestrator.
Activities still need to be idempotent because the underlying work-item delivery is at-least-once. A worker can complete an external side effect and then fail before the framework records completion. A visibility timeout, host restart, or retry policy can then cause the same logical activity to run again.
In other words:
Replay rebuilds orchestration state. At-least-once delivery creates the duplicate activity risk.
That difference does not change the design rule, but it does help us reason about failures correctly.
Design Activities for Idempotency
For every activity, ask what happens when it receives the same logical request twice.
If the answer is two payments, two emails, two inventory adjustments, or two customer records, the activity is not production-ready.
Good idempotency options include:
- Use naturally idempotent operations such as an upsert or a PUT-style update.
- Carry a stable operation ID or idempotency key into the activity.
- Enforce uniqueness atomically in the system that owns the data.
- Record the operation and the business change in the same transaction where possible.
- If calling an external API, use its native idempotency-key support.
- Return the original outcome when the same operation ID is seen again.
A simple “check whether it exists, then insert” is not enough when two executions can race. The final protection should normally be an atomic constraint, transaction, compare-and-set operation, or equivalent control at the data boundary.
Idempotency also starts at the client. If the same message or HTTP request can start the workflow twice, consider whether a business-derived instance ID, deduplication record, or upstream idempotency key is required. Do not place customer data or secrets in instance IDs; instance IDs show up across operational surfaces.
Final Thoughts
Durable Functions is easier to use well once the runtime behaviour is no longer hidden. Starters open the door, orchestrators coordinate, activities perform the work, and the Task Hub preserves the state required to recover and continue.
The core rules follow directly from that model: orchestrators must be deterministic, replay is not the same as activity re-execution, and side effects must tolerate duplicate delivery.
In Part 2, I will build on these foundations and explore function chaining, fan-out/fan-in, async HTTP APIs, monitors, external events, sagas, eternal orchestrations, sub-orchestrations, and durable entities.
What part of the Durable Functions execution model caused the biggest surprise for you? If I have missed a foundational concept that would help someone new to Durable Functions, please leave a comment or reach out.
Leave a comment