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

Created 21 Oct 2026 · Modified 23 Oct 2026 · 1 version

Durable Functions

Pick a backend

BackendUse whenNotes
Durable Task Scheduler (recommended)New apps, any Functions planManaged 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 appsQueues, tables and blobs in a storage account you own; 1–16 partitions (default 4), set when the task hub is created
MSSQLYou need SQL ServerIdentity-based connections need runtime-driven scaling
NetheriteExisting apps onlySupport 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> (omit ClientID for a system-assigned identity) and TASKHUB_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 CannotDeleteResource Cannot 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

SymptomCause → fix
Stuck in PendingThe 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 RunningWaiting 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 slowlyAzure Storage provider polling and partition rebalancing (on Flex, lower the polling interval); CPU or memory pressure
Scheduler access denied, or the app can't connectMissing 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 deployRequired app settings (connection string, TASKHUB_NAME) missing

Read this document as markdown — the same bytes an agent receives.