Blog
Published on 2 June 2026 3 min read

Isolated Worker in .NET 9: what changes and why it affects your CI/CD pipeline

A practical look at the Isolated Worker model in .NET 9 Azure Functions, the impact on local development, and how to update your GitHub Actions workflow accordingly.

dotnet azure-functions ci-cd devops
This article is also available in Dutch .

From in-process to Isolated Worker

Up through .NET 6, most Azure Functions code ran in-process alongside the Functions host. Convenient, but limiting: you were tied to the dependency versions the host shipped with, and upgrading to a new .NET version meant waiting for Microsoft.

With the Isolated Worker model, your function code runs in a separate process. The host is still responsible for receiving triggers and bindings, but communicates with your worker process over gRPC. This gives you full control over the runtime and your own dependencies.

In .NET 9, the in-process model has been officially retired. If you are still running in-process, now is the time to migrate.

What changes in practice

The most visible change is in your Program.cs. Where you previously had an empty startup class (or none at all), you now have an explicit host builder:

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices(services =>
    {
        services.AddApplicationInsightsTelemetryWorkerService();
        services.ConfigureFunctionsApplicationInsights();
    })
    .Build();

await host.RunAsync();

Middleware now works as a proper pipeline, similar to ASP.NET Core. You can intercept request handling, centralize error handling, and implement authentication generically:

.ConfigureFunctionsWorkerDefaults(worker =>
{
    worker.UseMiddleware<ExceptionHandlingMiddleware>();
})

Binding attributes are mostly the same, but the return type of your functions differs. You now return an HttpResponseData instead of an IActionResult:

[Function("Contact")]
public async Task<HttpResponseData> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req)
{
    var response = req.CreateResponse(HttpStatusCode.OK);
    await response.WriteAsJsonAsync(new { success = true });
    return response;
}

Impact on your CI/CD pipeline

Switching to Isolated Worker has direct consequences for how your build and deploy pipeline looks.

Build step: you still use dotnet publish, but make sure your target framework is correct. For .NET 9 Isolated:

<TargetFramework>net9.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>

GitHub Actions workflow: the azure/functions-action works fine with Isolated Worker, but pay attention to the package parameter โ€” it must point to the published output, not the source directory:

- name: Deploy Azure Functions
  uses: azure/functions-action@v1
  with:
    app-name: ${{ vars.FUNCTION_APP_NAME }}
    package: ./api/publish
    publish-profile: ${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}

Local development: func start still works, but you need to build first. Add a watch step to your dev workflow:

dotnet watch --project api/ build &
func start --csharp

Or use Tasks in .vscode/tasks.json to automate this.

Testability

An underappreciated benefit of Isolated Worker is improved testability. Because your functions are plain classes with constructor injection, you can unit test them without spinning up the Functions host.

Using xUnit and FluentAssertions:

[Fact]
public async Task Contact_ValidPayload_Returns200()
{
    // Arrange
    var function = new ContactFunction(_mockMailService.Object, _mockLogger.Object);
    var req = TestHttpRequestFactory.CreatePostRequest(validPayload);

    // Act
    var response = await function.Run(req);

    // Assert
    response.StatusCode.Should().Be(HttpStatusCode.OK);
}

The migration takes an afternoon, but you gain structurally better testability, long-term stability, and a cleaner pipeline in return.