External Events and Timers

Earn 25 points (50 with Pro) in two steps

  1. ① Read through the lesson — each section gets a ✓ as you scroll through it.
  2. ② When every section has a ✓, tap Complete lesson.

0 of 10 read · keep scrolling

✦ See fewer ads and earn double points — 50 a lesson instead of 25 — with Pro

Mastering External Events and Timers in Azure Durable Functions

Introduction: The Challenge of State and Time in Distributed Systems

In the world of cloud-native development, we often deal with processes that don't finish in a single request-response cycle. Imagine a business workflow where a user submits an application, but that application needs to be reviewed by a manager, or perhaps it requires a payment confirmation from a third-party gateway that might take hours or even days to arrive. Traditional serverless functions, which are stateless and short-lived, struggle with these scenarios because they lack a built-in mechanism to "pause" execution and wait for an outside signal. This is where Azure Durable Functions changes the game.

Durable Functions allow you to write stateful workflows in a serverless environment. By providing an orchestration layer, they enable you to maintain the state of a long-running process across multiple function executions. Two of the most powerful features within this framework are External Events and Durable Timers. External events allow your orchestration to "listen" for input from the outside world during its execution, while durable timers allow your code to wait for a specific duration without consuming compute resources. Together, these features empower developers to build complex, human-in-the-loop, or time-sensitive workflows that would otherwise require complex database state management or polling architectures.

Understanding how to implement these features is essential for any cloud engineer. Without them, you would likely find yourself building "state machines" manually, storing intermediate progress in tables or queues, and writing complex logic to handle timeouts and retries. By mastering the native capabilities of Durable Functions, you shift the burden of state management and time tracking to the Azure platform, allowing you to focus on the business logic that actually delivers value.


Not read yet

Understanding External Events

An external event is a way for an orchestrator function to pause its execution and wait for an arbitrary message from an external source. Unlike a standard function trigger, which fires when an HTTP request or a queue message arrives, the orchestrator waits at a specific line of code until a corresponding event name is raised. This is technically implemented via the WaitForExternalEvent method.

How External Events Work

When an orchestrator reaches a WaitForExternalEvent call, the execution of the orchestrator function effectively "checkpoints" and stops. The compute resources used by that specific execution are released back to the pool. When a message is sent to the orchestration instance—using the RaiseEventAsync method—the Durable Task framework wakes up the orchestrator, rehydrates its state, and resumes execution from exactly where it left off, passing the data from the external event into your code.

Callout: Orchestrator Checkpointing It is important to remember that orchestrator functions are replayed. When you use WaitForExternalEvent, the orchestrator function is effectively "completed" from the perspective of the runtime until the event arrives. When the event is raised, the orchestrator starts from the beginning again, re-executing your code to reach the state it was in previously, and then proceeds with the data provided by the event. This is why it is critical that your orchestrator code remains deterministic.

Practical Example: Human-in-the-Loop Approval

Imagine a document approval workflow. A user submits a document, and the system must wait for a manager to click an "Approve" or "Reject" button.

[FunctionName("ApprovalWorkflow")]
public static async Task RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    // 1. Send the approval request (e.g., via email or notification)
    await context.CallActivityAsync("SendApprovalRequest", "document_id_123");

    // 2. Wait for the external event "ApprovalResult"
    // This will pause the function until the event is raised
    string approvalResult = await context.WaitForExternalEvent<string>("ApprovalResult");

    // 3. Act on the result
    if (approvalResult == "Approved")
    {
        await context.CallActivityAsync("ProcessDocument", "document_id_123");
    }
    else
    {
        await context.CallActivityAsync("NotifyRejection", "document_id_123");
    }
}

In this example, the WaitForExternalEvent call keeps the workflow suspended. You can have a separate HTTP-triggered function that acts as the API endpoint for the manager's action:

[FunctionName("RaiseApprovalEvent")]
public static async Task<HttpResponseMessage> HttpStart(
    [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestMessage req,
    [DurableClient] IDurableOrchestrationClient client)
{
    string instanceId = req.GetQueryNameValuePairs().FirstOrDefault(q => q.Key == "instanceId").Value;
    string eventData = await req.Content.ReadAsStringAsync();

    // Raise the event to the specific orchestration instance
    await client.RaiseEventAsync(instanceId, "ApprovalResult", eventData);

    return new HttpResponseMessage(HttpStatusCode.Accepted);
}

Not read yet

Mastering Durable Timers

While external events handle asynchronous human or system signals, durable timers handle time-based delays. You might need to wait for 24 hours before sending a follow-up email, or you might need to implement a "timeout" mechanism where, if an external event doesn't arrive within a certain period, the workflow takes an alternative path.

Implementing Timers

Durable timers are created using the CreateTimer method. Unlike a standard Task.Delay in C#, which would keep a thread alive and consume memory while waiting, a durable timer is managed by the Durable Task framework. It informs the framework to wake the orchestrator up after a specific DateTime has passed.

Practical Example: Implementing a Timeout

A common pattern is combining a timer with an external event. If the event arrives first, we proceed; if the timer expires first, we cancel the wait and move to an error-handling state.

[FunctionName("WorkflowWithTimeout")]
public static async Task RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var timeout = context.CurrentUtcDateTime.AddMinutes(30);
    var timerTask = context.CreateTimer(timeout, context.CurrentUtcSettings.CancellationToken);
    var approvalTask = context.WaitForExternalEvent<string>("ApprovalResult");

    // Wait for either the event or the timer to complete
    Task winner = await Task.WhenAny(approvalTask, timerTask);

    if (winner == approvalTask)
    {
        // Success: Approval received within 30 minutes
        string result = approvalTask.Result;
        await context.CallActivityAsync("HandleApproval", result);
    }
    else
    {
        // Timeout: 30 minutes passed without approval
        await context.CallActivityAsync("HandleTimeout", "No response received in time");
    }
}

Note: Determinism in Timers Always use context.CurrentUtcDateTime instead of DateTime.UtcNow. Using the standard .NET DateTime class inside an orchestrator will break the replay logic because the time will change every time the orchestrator replays, leading to non-deterministic behavior. context.CurrentUtcDateTime provides a consistent "orchestrator time" that remains the same across all replays for a given point in the workflow.


Not read yet

Best Practices and Industry Recommendations

When working with external events and timers, architecture matters. Because orchestrators can run for days or weeks, you need to ensure that your code is resilient to changes and that your instance management is robust.

1. Naming Events Consistently

Always define event names as constants. If you have multiple workflows waiting for different events, it is easy to accidentally raise an event for the wrong orchestration instance or use the wrong event name. Centralizing these strings in a shared class or configuration file prevents typos and makes refactoring much easier.

2. Handling Multiple Events

Sometimes, a workflow might need to wait for multiple events, or it might receive events in an unexpected order. You can use Task.WhenAll or Task.WhenAny to manage complex event flows. For example, if you are building an integration that waits for both a "PaymentReceived" and a "ShippingConfirmed" event, you can orchestrate these using standard C# task patterns.

3. Instance IDs are Crucial

The instanceId is the unique identifier for your orchestration. When you start an orchestration, you should generate an ID that relates to your business entity (e.g., Order_12345). This makes it trivial to raise events from other parts of your system without needing to query the Durable Functions status API to find the ID.

4. Avoiding "Zombie" Events

If you raise an event to an orchestration instance that has already completed or terminated, the event is effectively lost. While the Durable Task framework handles this gracefully (it simply ignores the event), you should design your system to handle these edge cases. Always check the status of an orchestration before attempting to raise an event if your system architecture allows for race conditions.

5. Keeping Orchestrators Lean

Orchestrators should only contain workflow logic. Do not put heavy data processing, database queries, or complex calculations inside the orchestrator. If you need to transform data, use an activity function. This ensures that the orchestrator stays lightweight and replays quickly, which is essential for performance and keeping your cloud costs low.


Not read yet

Common Pitfalls and How to Avoid Them

Even with experience, developers often fall into traps when dealing with long-running processes. Below are the most frequent mistakes observed in production environments.

Race Conditions with Events

A common mistake is assuming that an event will be available immediately. If an orchestrator starts and immediately calls WaitForExternalEvent, but the event was raised a millisecond before the orchestrator was ready, the framework will buffer the event. However, if your logic is flawed, you might end up in a state where the orchestrator is waiting for an event that already occurred or has timed out. Always design your workflows to be "re-entrant" and capable of checking the state of the system before waiting.

Non-Deterministic Logic

As mentioned, the orchestrator code is replayed multiple times. If you use Guid.NewGuid(), DateTime.Now, or any random number generator, your orchestrator will fail. The framework detects these non-deterministic operations and will throw an error during execution.

  • Fix: Use context.NewGuid() and context.CurrentUtcDateTime provided by the IDurableOrchestrationContext.

Forgetting to Handle Cancellation

If your timer expires, you should ensure that any pending tasks are cancelled. If you don't use the CancellationToken provided by the timer, you might end up with "zombie" tasks still running in the background. Proper cleanup is vital for maintaining a clean and cost-effective cloud environment.

Over-reliance on External Events

Sometimes developers use External Events for communication between functions when standard messaging (like Azure Service Bus or Storage Queues) would be more appropriate. External Events are specifically designed for orchestration-level signals. If you are just passing data from one function to another, use queues or topics.


Not read yet

Quick Reference: Comparison of Execution Patterns

Feature Best Used For Key Constraint
Activity Functions Data processing, I/O, DB operations Must be deterministic
External Events Human-in-the-loop, async callbacks Requires unique instance ID
Durable Timers Timeouts, delays, scheduling Use context.CurrentUtcDateTime
Sub-Orchestrations Breaking down complex workflows Increases complexity

Step-by-Step: Implementing a Robust Timeout Pattern

Follow these steps to implement a reliable timeout mechanism in your Durable Function.

  1. Define your constants: Create a static class to hold your event names.
    public static class EventNames {
        public const string ApprovalReceived = "ApprovalResult";
    }
    
  2. Initialize the Orchestration: Start the orchestration with a clear InstanceId based on your business key (e.g., Order_98765).
  3. Create the Timer: Within the orchestrator, define your timeout duration (e.g., 24 hours).
    DateTime deadline = context.CurrentUtcDateTime.AddHours(24);
    using (var cts = new CancellationTokenSource()) {
        Task timerTask = context.CreateTimer(deadline, cts.Token);
        Task<string> eventTask = context.WaitForExternalEvent<string>(EventNames.ApprovalReceived);
        
        // Wait for the first one to finish
        Task winner = await Task.WhenAny(timerTask, eventTask);
        
        if (winner == eventTask) {
            cts.Cancel(); // Cancel the timer if the event arrived
            // Process success
        } else {
            // Handle timeout
        }
    }
    
  4. Testing: Use the Durable Functions Monitor tool or the Azure CLI to manually raise events during testing to ensure your timeout logic triggers correctly.
  5. Monitoring: Use Application Insights to track the lifecycle of your orchestration instances. Look for "OrchestratorCompleted" and "OrchestratorFailed" events to ensure your workflows are closing correctly.

Not read yet

Advanced Scenarios: Handling Multiple Events

What happens if you need to wait for a sequence of events? For example, an order process that requires both a "PaymentReceived" event and a "InventoryReserved" event. You can wait for these concurrently.

var paymentTask = context.WaitForExternalEvent<bool>("PaymentReceived");
var inventoryTask = context.WaitForExternalEvent<bool>("InventoryReserved");

// Wait for both to complete
await Task.WhenAll(paymentTask, inventoryTask);

if (paymentTask.Result && inventoryTask.Result) {
    // Proceed to shipping
}

This pattern is highly efficient because it does not require you to write complex state-tracking code. The Durable Task framework manages the persistence of these "wait" states. If the function app crashes or restarts, the framework will resume exactly where it was, knowing that it is still waiting for those specific events.

Callout: Scaling Considerations Because Durable Functions are built on top of Azure Storage, every event raised is stored as a message in a queue. If you are raising thousands of events per second, consider the throughput limits of your storage account. For most enterprise workflows, this is not an issue, but for high-frequency event processing, ensure your storage account is configured for high performance.


Not read yet

Integration with External Systems

External events are the primary bridge between your Durable Function and the outside world. Whether you are integrating with a webhook from Stripe, a callback from a CRM like Salesforce, or a manual button click in a custom web portal, the pattern remains identical.

Webhook Integration Example

If you are receiving a webhook from a third-party service:

  1. Expose an HTTP endpoint: Create an HTTP-triggered function in your Function App.
  2. Validate the payload: Ensure the incoming data is secure and authentic (verify signatures).
  3. Raise the event: Use the IDurableOrchestrationClient to call RaiseEventAsync with the data from the webhook.
  4. Orchestrator resumes: The orchestrator will wake up and use the payload to continue the workflow.

This decoupling is powerful because the third-party service doesn't need to know about your internal orchestration logic. It only needs to send a standard HTTP POST request to your endpoint.


Not read yet

Common Questions and FAQ

Q: Can I raise an event to an orchestration that is currently running? A: Yes. If the orchestrator is currently executing, the event will be queued and processed the next time the orchestrator reaches a WaitForExternalEvent call or completes its current execution.

Q: What happens if I raise an event but the orchestration is not waiting for it? A: The event will be stored in the orchestration instance's history. The next time the orchestrator reaches a WaitForExternalEvent call, it will check its history, find the event, and consume it immediately without pausing. This is a very useful feature for handling events that might arrive slightly before the orchestration is ready.

Q: Is there a limit to how long an orchestrator can wait? A: There is no strict hard limit on the duration of a wait. However, you should be mindful of the storage cost of maintaining long-running instances. If you have millions of instances waiting for years, your storage costs may grow.

Q: Can I cancel an orchestration from the outside? A: Yes, you can use the TerminateAsync method on the IDurableOrchestrationClient. This will immediately stop the orchestration and record its status as "Terminated."


Not read yet

Summary and Key Takeaways

Mastering external events and timers is the difference between writing simple scripts and building robust, enterprise-grade distributed systems. By leveraging these features, you can handle complex, asynchronous business processes with minimal code and maximum reliability.

Key Takeaways:

  1. Stateful Orchestration: External events and timers allow you to pause and resume workflows, effectively offloading state management to the Azure Durable Task framework.
  2. Deterministic Execution: Always use context-provided methods like context.CurrentUtcDateTime and context.NewGuid() to ensure your orchestrator functions remain deterministic across replays.
  3. Event Handling: Use WaitForExternalEvent to pause workflows for human or system signals. Remember that events are queued if they arrive before the orchestrator is ready to receive them.
  4. Timeout Patterns: Combine CreateTimer with Task.WhenAny to implement robust timeout logic, ensuring your workflows don't hang indefinitely.
  5. Instance Management: Always use meaningful, business-centric InstanceIDs to make debugging, logging, and event raising straightforward.
  6. Avoid Anti-patterns: Keep your orchestrators lean by offloading data processing to activity functions and avoid non-deterministic operations at all costs.
  7. Resilience: Leverage the fact that Durable Functions are naturally resilient to infrastructure failures; the platform handles the checkpointing and rehydration for you, ensuring that your long-running processes eventually complete.

By applying these concepts, you will be able to build workflows that are not only easier to maintain but also significantly more capable of handling the messy, real-world requirements of modern distributed applications. Start small by implementing a simple timer, then move on to external events, and you will quickly see how these tools simplify your architecture.

Not read yet

Each section gets a ✓ as you scroll through it. Tap the button to jump to the next one.