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
HostBuilderpattern 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:
OutputTypeis nowExe— your function app is a standalone executable.- The old
Microsoft.NET.Sdk.Functionspackage is gone, replaced byMicrosoft.Azure.Functions.WorkerandMicrosoft.Azure.Functions.Worker.Sdk. - All the
WebJobs.Extensions.*packages are replaced by theirFunctions.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
outparameters 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.Jsonby default, notNewtonsoft.Json. If your models rely on Newtonsoft attributes, you'll need to either switch or configure the worker to use Newtonsoft. - No more
ILoggerinjection via method parameter — In the In-Process model, you could addILogger logas a function parameter. In Isolated Worker, injectILogger<T>orILoggerFactorythrough the constructor. - SignalR binding changes — The
SignalRConnectionInfoInputreplaces the oldSignalRConnectionInfoattribute, andSignalROutputwithSignalRMessageActionreplaces the oldSignalRMessagereturn 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
HostBuilderpattern. - Replace all attributes:
[FunctionName]→[Function],[CosmosDB]→[CosmosDBInput]/[CosmosDBOutput], etc. - Switch from
HttpRequesttoHttpRequestDataand adapt your response patterns. - Set
FUNCTIONS_WORKER_RUNTIMEtodotnet-isolatedin 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!