Skip to content

ObjectDisposedException during host shutdown #3452

Description

@andrey-malkov

Problem Description

We are running Durable Functions orchestrations on Azure Functions Isolated Worker (.NET 10, Premium plans, Linux). When the Azure Functions platform performs a scale-in (removes a worker instance), in-flight orchestrator functions throw System.ObjectDisposedException: Cannot access a disposed object. Object name: 'IServiceProvider' and are permanently marked as RuntimeStatus = Failed. The Durable Task framework does not retry these orchestrations — they remain in Failed state indefinitely, causing upstream workflows to become stuck.

The cascade

When the host initiates shutdown for scale-in, the DI ServiceProvider is disposed. However, orchestrator functions that have already been dispatched (messages dequeued from the control queue and handed to the worker) attempt to resolve services via DefaultFunctionContext.get_InstanceServices() which calls ServiceProvider.CreateScope(). This throws ObjectDisposedException because the container is already disposed.

The Durable Task extension treats this as an unhandled orchestrator failure and commits RuntimeStatus = Failed to the Instances table. The failure then propagates up the orchestration call chain:

  1. wait-for-task (sub-orchestration) — fails first with ObjectDisposedException
  2. migrate-site-workflow (parent) — receives SubOrchestrationInstanceFailed from wait-for-task and also transitions to Failed
  3. process-one-backlog-item-workflow (top-level) — receives the cascading failure and permanently fails

Inconsistent retry behavior: In some cases, the framework appears to retry the failed orchestration — the queue message is picked up by another instance and the workflow recovers. However, in other cases (the majority we observe), the failure is permanent: RuntimeStatus = Failed is committed to the Instances table and no retry occurs. The business workflow is permanently abandoned with no automatic recovery.

The critical issue: Unlike activity function failures which leave queue messages available for retry, orchestrator failures sometimes commit a terminal Failed status to the instance store. Once committed, the orchestration is permanently dead — no re-dispatch. The non-deterministic nature of retry vs permanent failure makes this particularly dangerous: there is no reliable way to predict which orchestrations will recover and which will be permanently lost.

Observed timeline (real production incident, 2026-06-01 13:45 UTC)

Time (UTC) Event
13:45:23.834 Host logs: "Application is shutting down..." on instance 0--bb3e733a...
13:45:24.015 29 orchestrations fetch TimerFired messages from control queues
13:45:24.051 Orchestrations begin processing (timer delays 63–64 seconds)
13:45:24.098 ObjectDisposedException ×28 thrown — ServiceProvider already disposed
13:45:24.155 wait-for-task orchestrations set to RuntimeStatus: Failed
13:45:24.165 SubOrchestrationInstanceFailed messages sent to parent orchestrations
13:45:24.197 Instance tables updated: 29 orchestrations permanently Failed
13:45:28.445 Parent process-one-backlog-item-workflow orchestrations also Failed

After shutdown, the instance never appeared again — confirming this was a permanent scale-in, not a restart.

Key observations

  1. The ServiceProvider is disposed before in-flight invocations complete. The shutdown signal and ServiceProvider disposal happen within 28–478ms of each other. Orchestrator functions already dispatched cannot resolve dependencies.

  2. Orchestrator failures are permanent — no retry. Unlike activity failures (which return the message to the queue), when an orchestrator function fails, the extension commits RuntimeStatus = Failed to the Instances table. The Durable Task framework has no built-in retry for orchestrator failures.

  3. One scale-in event kills many orchestrations. In the worst observed incident, a single instance shutdown caused 29 sub-orchestrations and their parent workflows to fail permanently. With many orchestrations sharing a single instance, one scale-in can cascade into dozens of permanently abandoned workflows.

  4. SubOrchestrationInstanceFailed propagates fatally. When a child wait-for-task orchestration fails, the parent process-one-backlog-item-workflow receives SubOrchestrationInstanceFailed and also transitions to Failed — extending the blast radius beyond the originally affected instance.

  5. Contrast: Timer-based orchestrations survive. Orchestrations that were merely waiting on timers (not actively executing) survive because their timer messages remain on the control queue and are picked up by a new instance. Only orchestrations whose code was actively executing at shutdown time are permanently lost.

Impact (3-day observation window, EU region only)

Metric Value
Total ObjectDisposedException occurrences 169
Unique worker instances affected (scale-in events) 7
Permanently failed orchestration instances 39
Affected function apps odmt-eu-b1-files-workers, odmt-eu-b1-mail-workers
Failed orchestration types wait-for-task, process-one-backlog-item-workflow
Customer impact Migration tasks stuck permanently — no completion

Extension log confirming permanent failure

739294d1f4be5a8ab7541cc46f088164: Function 'wait-for-task (Orchestrator)' failed with an error.
Reason: Microsoft.Azure.WebJobs.Host.FunctionInvocationException
IsReplay: False. State: Failed. RuntimeStatus: Failed.
HubName: filesworkers. AppName: odmt-eu-b1-files-workers.
SlotName: Production. ExtensionVersion: 3.12.5. SequenceNumber: 16420. TaskEventId: -1

Exception stack trace

Microsoft.Azure.WebJobs.Host.FunctionInvocationException:
  Exception while executing function: Functions.process-one-backlog-item-workflow
 ---> Microsoft.Azure.WebJobs.Script.Workers.Rpc.RpcException:
  Result: Failure
  Type: System.ObjectDisposedException
  Exception: Cannot access a disposed object.
  Object name: 'IServiceProvider'.
  Stack:
    at Microsoft.Extensions.DependencyInjection.ServiceLookup.ThrowHelper
       .ThrowObjectDisposedException()
    at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateScope()
    at Microsoft.Azure.Functions.Worker.DefaultFunctionContext
       .get_InstanceServices()
       in /*/src/DotNetWorker.Core/Context/DefaultFunctionContext.cs:line 46
    at Microsoft.Extensions.Hosting.MiddlewareWorkerApplicationBuilderExtensions
       .<>c__DisplayClass3_0`1.b__1(FunctionContext context)
       in /_/src/DotNetWorker.Core/Hosting/WorkerMiddlewareWorkerApplicationBuilderExtensions.cs:line 105
    at Microsoft.Azure.Functions.Worker.FunctionsApplication
       .InvokeFunctionAsync(FunctionContext context)
       in /_/src/DotNetWorker.Core/FunctionsApplication.cs:line 83
    at Microsoft.Azure.Functions.Worker.Handlers.InvocationHandler
       .InvokeAsync(InvocationRequest request)
       in /_/src/DotNetWorker.Grpc/Handlers/InvocationHandler.cs:line 91

Expected behavior

When a Functions host undergoes shutdown/scale-in:

  1. Pending orchestrator messages should be abandoned (left on queue) so another instance picks them up
  2. If an orchestrator function invocation fails due to host disposal, the failure should be treated as retriable (not permanently terminal)
  3. The ServiceProvider disposal should be deferred until all in-flight function invocations complete (graceful drain)

Product: Durable Azure Functions (Isolated Worker, .NET 10)
Plan: Flex Consumption
Durable Task Extension: Microsoft.Azure.Functions.Worker.Extensions.DurableTask 1.16.5
Task Hub: filesworkers, mailboxworkers
OS: Linux
Regions affected: North Europe


Metadata

Metadata

Assignees

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions