Azure Functions: Migrating to the Isolated Worker Model

Why the Isolated Worker Model is now the standard and how to migrate from In-Process 08/25/2024

Earlier this year, while preparing a presentation for the Global Azure Bootcamp, I had to update my demo application: a real-time voting app built with Azure Functions, Cosmos DB and SignalR. The app had been running on Azure Functions SDK v2 with the In-Process model for years. It was time to migrate it to SDK v4 with the Isolated Worker Model — and honestly, it went smoother than I expected.

You can find the full source code of the migrated application on my GitHub repository: github.com/yfrouin/azure-voting-app. In this article, I'll walk you through what the Isolated Worker Model is, why it's now the standard, and how I migrated my app step by step.

In-Process vs Isolated Worker: what's the difference?

In the early days of Azure Functions on .NET, your function code ran inside the same process as the Functions host. This is called the In-Process model. It was convenient — you had access to the same types, the same dependency injection container, and everything felt tightly integrated.

The Isolated Worker Model (also called out-of-process) changes the game: your function code runs in a separate .NET worker process, decoupled from the Functions host. Communication between the host and your worker happens through gRPC.

Think of it like this: In-Process is like living with your parents — convenient but you share everything and have limited freedom. Isolated Worker is getting your own apartment — a bit more setup, but you control your own dependencies, your own .NET version, and your own middleware pipeline.

Why migrate? Because you have to (and should want to)

Let's be clear: the In-Process model is deprecated. Microsoft has announced that support for the In-Process model will end with the retirement of .NET 8 (LTS) support in Azure Functions. The Isolated Worker Model is the only supported model going forward.

But beyond the "you must", there are excellent reasons to actually want to migrate:

  • Full control over .NET version — No more waiting for the Functions host to support the latest .NET. You choose your target framework.
  • Custom middleware pipeline — Just like ASP.NET Core, you can add middleware for logging, authentication, error handling, etc.
  • Dependency isolation — No more assembly version conflicts between your code and the host. This was a real pain point with In-Process.
  • Standard .NET hosting patterns — The HostBuilder pattern feels familiar to any ASP.NET Core developer.
  • Better testability — Functions are regular classes with regular methods. Much easier to unit test.

The migration: my voting app journey

My azure-voting-app is a real-time voting application that uses:

  • Azure Functions — HTTP triggers for the API, Cosmos DB triggers and bindings for data, SignalR for real-time updates
  • Azure Cosmos DB — Stores the votes
  • Azure SignalR Service — Pushes real-time vote updates to the Angular client

The app was originally on Functions SDK v2 targeting .NET Core. I migrated it to SDK v4 targeting .NET 7 with the Isolated Worker Model. Here's how it went.

Step 1: Update the project file

The first and most impactful change is in the .csproj file. The SDK, target framework, output type, and NuGet packages all change.

Here's what the old In-Process project looked like:


<!-- OLD: In-Process SDK v2 -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <AzureFunctionsVersion>v2</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.29" />
    <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.CosmosDB" Version="3.0.7" />
    <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.SignalRService" Version="1.2.0" />
  </ItemGroup>
</Project>
                

And here's the new Isolated Worker project:


<!-- NEW: Isolated Worker SDK v4 -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
    <OutputType>Exe</OutputType>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.14.1" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.10.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.0.13" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.CosmosDB" Version="4.0.1" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.SignalRService" Version="1.7.0" />
  </ItemGroup>
</Project>
                

Key changes to notice:

  • OutputType is now Exe — your function app is a standalone executable.
  • The old Microsoft.NET.Sdk.Functions package is gone, replaced by Microsoft.Azure.Functions.Worker and Microsoft.Azure.Functions.Worker.Sdk.
  • All the WebJobs.Extensions.* packages are replaced by their Functions.Worker.Extensions.* equivalents.

Step 2: Add the Program.cs entry point

In the In-Process model, there was no Program.cs — the Functions host managed everything. In the Isolated Worker Model, your app needs an explicit entry point, just like any .NET console application:


using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .Build();

host.Run();
                

This is where the magic happens. The HostBuilder gives you the same patterns you know from ASP.NET Core. Need to register services? Add middleware? Configure logging? It all goes here:


using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(worker =>
    {
        // Add custom middleware here
        // worker.UseMiddleware<MyCustomMiddleware>();
    })
    .ConfigureServices(services =>
    {
        // Register your services
        // services.AddSingleton<IMyService, MyService>();
    })
    .Build();

host.Run();
                

Step 3: Update the function signatures

This is where most of the work happens. The attributes and types change between In-Process and Isolated Worker. Let me show you the before and after for each of my functions.

HTTP Trigger + Cosmos DB Input (GetVotes)

The old In-Process way used IActionResult and FunctionName:


// OLD: In-Process
[FunctionName("GetVotes")]
public static IActionResult GetVotesFunction(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get",
        Route = "vote/{eventName}")] HttpRequest req,
    [CosmosDB(databaseName: "Votes", collectionName: "Votes",
        ConnectionStringSetting = "CosmosDBConnection",
        SqlQuery = "SELECT * FROM Votes where Votes.eventName = {eventName} order by Votes.id desc")]
        IEnumerable<Vote> votes)
{
    return new OkObjectResult(votes);
}
                

The new Isolated Worker way uses HttpRequestData and Function:


// NEW: Isolated Worker
[Function("GetVotes")]
public static IEnumerable<Vote> GetVotesFunction(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get",
        Route = "vote/{eventName}")] HttpRequestData req,
    [CosmosDBInput(
        databaseName: "Votes",
        containerName: "Votes",
        Connection = "CosmosDBConnection",
        SqlQuery = "SELECT * FROM Votes where Votes.eventName = {eventName} order by Votes.id desc")]
        IEnumerable<Vote> votes)
{
    return votes;
}
                

HTTP Trigger + Cosmos DB Output (CreateVote)


// NEW: Isolated Worker — Creating a vote with Cosmos DB output binding
[Function("CreateVote")]
public static async Task<VoteResponse> CreateVoteFunction(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "vote")] HttpRequestData req)
{
    var vote = await req.ReadFromJsonAsync<Vote>();
    vote!.id = Guid.NewGuid().ToString();

    var response = req.CreateResponse(HttpStatusCode.OK);

    return new VoteResponse()
    {
        Vote = vote!,
        HttpResponse = response
    };
}
                

Notice the multi-output pattern with VoteResponse. In the Isolated Worker Model, when a function needs to return both an HTTP response and write to a binding (like Cosmos DB), you use a POCO class with multiple output properties:


public class VoteResponse
{
    [HttpResult]
    public HttpResponseData HttpResponse { get; set; }

    [CosmosDBOutput(databaseName: "Votes", containerName: "Votes",
        Connection = "CosmosDBConnection")]
    public Vote Vote { get; set; }
}
                

Cosmos DB Trigger + SignalR Output (real-time updates)


// NEW: Isolated Worker — Cosmos DB change feed triggers SignalR broadcast
[Function("CosmosDBTrigger")]
[SignalROutput(HubName = "votes")]
public static SignalRMessageAction Run(
    [CosmosDBTrigger(
        databaseName: "Votes",
        containerName: "Votes",
        Connection = "CosmosDBConnection",
        LeaseContainerName = "VotesLeases",
        CreateLeaseContainerIfNotExists = true)] IReadOnlyList<Vote> votes)
{
    return new SignalRMessageAction("votesUpdated", new[] { votes });
}
                

SignalR Negotiate


// NEW: Isolated Worker — SignalR connection negotiation
[Function("negotiate")]
public static string Negotiate(
    [HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequestData req,
    [SignalRConnectionInfoInput(HubName = "votes")] string connectionInfo)
{
    return connectionInfo;
}
                

Step 4: Update local.settings.json

One small but critical change: the FUNCTIONS_WORKER_RUNTIME value must be set to dotnet-isolated instead of dotnet:


{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "CosmosDBConnection": "your-cosmosdb-connection-string",
    "AzureSignalRConnectionString": "your-signalr-connection-string"
  }
}
                

If you forget this, you'll get a very confusing error at startup. Ask me how I know.

Key differences cheat sheet

Here's a summary of the major attribute and type changes:

Concept In-Process Isolated Worker
Function attribute [FunctionName("...")] [Function("...")]
HTTP request type HttpRequest HttpRequestData
HTTP response type IActionResult HttpResponseData
Cosmos DB input [CosmosDB(...)] [CosmosDBInput(...)]
Cosmos DB output [CosmosDB(...)] on out param [CosmosDBOutput(...)]
Collection parameter collectionName containerName
Connection parameter ConnectionStringSetting Connection
Entry point None (host managed) Program.cs with HostBuilder
Worker runtime dotnet dotnet-isolated

Gotchas and lessons learned

Here are a few things that caught me off guard during the migration:

  • Multi-output bindings — In the In-Process model, you could use out parameters for output bindings. In Isolated Worker, you need a response POCO class with attributed properties. It's cleaner, but different.
  • JSON serialization — The Isolated Worker uses System.Text.Json by default, not Newtonsoft.Json. If your models rely on Newtonsoft attributes, you'll need to either switch or configure the worker to use Newtonsoft.
  • No more ILogger injection via method parameter — In the In-Process model, you could add ILogger log as a function parameter. In Isolated Worker, inject ILogger<T> or ILoggerFactory through the constructor.
  • SignalR binding changes — The SignalRConnectionInfoInput replaces the old SignalRConnectionInfo attribute, and SignalROutput with SignalRMessageAction replaces the old SignalRMessage return type.
  • Cold start — The Isolated Worker model may have slightly longer cold starts since it spins up a separate process. In practice, with proper warm-up and premium plans, this is negligible.

Summary

Migrating from the In-Process model to the Isolated Worker Model is not just a recommended upgrade — it's a necessary one. The In-Process model is on its way out, and the Isolated Worker Model brings genuine improvements: better dependency management, full .NET version control, custom middleware, and cleaner architecture patterns.

In my case, migrating the azure-voting-app from SDK v2 to v4 took about a day, including updating the Angular client app. The migration was straightforward once I understood the attribute mapping and the multi-output pattern. It was also a great excuse to clean up some old code — every migration is a refactoring opportunity in disguise.

Key takeaways:

  • The In-Process model is deprecated. Migrate before you're forced to.
  • Update your .csproj: new packages, OutputType Exe, target the .NET version you want.
  • Add a Program.cs with the HostBuilder pattern.
  • Replace all attributes: [FunctionName][Function], [CosmosDB][CosmosDBInput]/[CosmosDBOutput], etc.
  • Switch from HttpRequest to HttpRequestData and adapt your response patterns.
  • Set FUNCTIONS_WORKER_RUNTIME to dotnet-isolated in your settings.
  • Use multi-output POCOs when you need both an HTTP response and a binding output.

For the official migration guide, check out the Microsoft documentation on migrating to the Isolated Worker Model. And feel free to browse my voting app repo for a complete working example!

Share On