Durable Functions — storage providers, task hubs, Durable Task Scheduler with managed identity, the Node.js model, and stuck orchestrations
Running Durable Functions on Azure — choosing a backend (Durable Task Scheduler recommended; Azure Storage; MSSQL; Netherite until March 31, 2028), task hub rules, the scheduler in Bicep with Durable Task Data roles, connection string and host.json, Flex advice, TypeScript with durable-functions 3.x, and fixes for stuck orchestrations.
markdownreferencev1B462-59WPstage: buildtrust: officialservice: functionsinterface: iacinterface: sdkDurable FunctionsDurable Task Schedulerdurabletask.ioMicrosoft.DurableTask/schedulerstaskHubsDurable Task Data ContributorDurable Task WorkerDurable Task Data ReaderDURABLE_TASK_SCHEDULER_CONNECTION_STRINGAuthentication=ManagedIdentitystorageProvider azureManagedTASKHUB_NAMEextension bundle 4.32.0task hub name rulestask hub shared between appspartitionCountNetherite end of support March 31 2028MSSQL storage providerdurable-functions npm 3df.app.orchestrationcallActivityWithRetryorchestrator deterministic codeorchestration stuck Pendingorchestration stuck RunningCannotDeleteResource nested resources task hubsUnknown error retrieving details of this task hubEncountered an error (ServiceUnavailable) from host runtime
Durable Functions
Pick a backend
| Backend | Use when | Notes |
|---|---|---|
| Durable Task Scheduler (recommended) | New apps, any Functions plan | Managed service with its own state store; gRPC to {name}.{region}.durabletask.io with Entra identity, no keys (Authentication=ManagedIdentity in Azure; DefaultAzure or AzureCLI locally); dashboard; private endpoints; Dedicated or Consumption SKU |
| Azure Storage (default) | Lowest-cost, already-built apps | Queues, tables and blobs in a storage account you own; 1–16 partitions (default 4), set when the task hub is created |
| MSSQL | You need SQL Server | Identity-based connections need runtime-driven scaling |
| Netherite | Existing apps only | Support ends March 31, 2028; not on Flex Consumption |
Task hubs hold an app's orchestration state. Names are alphanumeric, 3–45 characters. Each app, and each staging slot, needs its own task hub: apps sharing one compete for messages and orchestrations get stuck. The default (the app name) avoids this; if you set hubName in host.json, make it an app setting (%TASKHUB_NAME%) and slot-sticky.
Durable Task Scheduler with a managed identity
// Durable Task Scheduler + task hub, with a user-assigned identity granted access to the hub
param location string = resourceGroup().location
param schedulerName string
param taskHubName string = 'orders'
param identityPrincipalId string
param ipAllowlist array // ranges allowed to reach the endpoint
resource scheduler 'Microsoft.DurableTask/schedulers@2026-02-01' = {
name: schedulerName
location: location
properties: {
ipAllowlist: ipAllowlist
sku: {
name: 'Consumption'
}
}
}
resource hub 'Microsoft.DurableTask/schedulers/taskHubs@2026-02-01' = {
parent: scheduler
name: taskHubName
}
// Durable Task Data Contributor
var durableTaskDataContributor = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0ad04412-c4d5-4796-b79c-f76d14c8d402')
resource hubAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
scope: hub
name: guid(hub.id, identityPrincipalId, durableTaskDataContributor)
properties: {
roleDefinitionId: durableTaskDataContributor
principalId: identityPrincipalId
principalType: 'ServicePrincipal'
}
}
output endpoint string = scheduler.properties.endpoint
- Roles: Durable Task Data Contributor (everything; most apps), Durable Task Worker (processing only), Durable Task Data Reader. Scope to the task hub; the scheduler scope covers all hubs. Without a role for yourself the dashboard shows
Unknown error retrieving details of this task hub. - App settings:
DURABLE_TASK_SCHEDULER_CONNECTION_STRING=Endpoint=<endpoint>;Authentication=ManagedIdentity;ClientID=<identity client ID>(omitClientIDfor a system-assigned identity) andTASKHUB_NAME. host.json:extensions.durableTask.storageProvider={ "type": "azureManaged", "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING" },hubName=%TASKHUB_NAME%. Non-.NET apps need extension bundle[4.32.0, 5.0.0)or later. (Microsoft's provider comparison table still says bundle support is "coming soon"; the quickstart is newer.)- Limits: schedulers and task hubs per region and subscription depend on the SKU; a Consumption scheduler holds at most five task hubs, and task hub quotas can't be raised. Each work-item completion is at most 3.9 MB; larger payloads need the large-payload feature.
- Deleting a scheduler with task hubs in it fails with
CannotDeleteResourceCannot delete resource while nested resources exist…: delete the task hubs first.
Azure Storage backend with identity: identity-based AzureWebJobsStorage (reference/functions/hosting-and-host-storage.md) plus Storage Queue, Blob and Table Data Contributor. On Flex Consumption, Microsoft advises one always-ready instance for the durable group and a queue polling interval of 10 seconds or less.
TypeScript (Node.js v4 model, durable-functions 3.x)
import { type HttpRequest, type HttpResponse, type InvocationContext } from "@azure/functions";
import * as df from "durable-functions";
import { type OrchestrationContext, type OrchestrationHandler, type ActivityHandler } from "durable-functions";
// Orchestrator: deterministic; yield tasks, never await.
const fanOut: OrchestrationHandler = function* (context: OrchestrationContext) {
const items = context.df.getInput<string[]>() ?? [];
const retry = new df.RetryOptions(5000, 3);
const tasks = items.map((id) => context.df.callActivityWithRetry("processItem", retry, id));
const results: number[] = yield context.df.Task.all(tasks);
return results.reduce((a, b) => a + b, 0);
};
df.app.orchestration("fanOut", fanOut);
const processItem: ActivityHandler = async (id: unknown, ctx: InvocationContext): Promise<number> => {
ctx.log(`processing ${String(id)}`);
return 1; // activities do the I/O
};
df.app.activity("processItem", { handler: processItem });
df.app.client.http("startFanOut", {
route: "orchestrators/fanOut",
methods: ["POST"],
extraInputs: [df.input.durableClient()],
handler: async (req: HttpRequest, client: df.DurableClient, _ctx: InvocationContext): Promise<HttpResponse> => {
const instanceId = await client.startNew("fanOut", { input: await req.json() });
return client.createCheckStatusResponse(req, instanceId);
},
});
Orchestrators replay from history, so they must be deterministic: use context.df.currentUtcDateTime, durable timers and activities instead of clocks, random values, network calls or await. Breaking changes to in-flight orchestrations leave them stuck: version them.
When orchestrations don't progress
| Symptom | Cause → fix |
|---|---|
Stuck in Pending | The start message isn't being picked up. Check Durable Task Framework traces for the instance ID in Application Insights; on Azure Storage, look in the <hub>-control-NN queues; another app on the same task hub may be taking the messages; memory (switch App Service to 64-bit) |
Stuck in Running | Waiting on an activity, timer or external event that never completes, or a breaking code change. Restart the app; look for one control queue that keeps growing |
| Starts late, or runs slowly | Azure Storage provider polling and partition rebalancing (on Flex, lower the polling interval); CPU or memory pressure |
| Scheduler access denied, or the app can't connect | Missing role on the hub, identity not attached to the app, or a wrong Endpoint/ClientID in the connection string |
Encountered an error (ServiceUnavailable) from host runtime on deploy | Required app settings (connection string, TASKHUB_NAME) missing |