Problem Context
Serverless on Azure used to be a tier choice: Consumption (cheap, cold starts, 5-min limit), Premium (warm, expensive, always-on instances), or App Service Plan (just pay for the plan). In 2025 Azure shipped Flex Consumption as the modern default β fast cold starts, per-second billing, virtual network integration, and concurrency you actually control.
At the same time the .NET in-process model is end-of-life β new functions run on the isolated worker, which lets you target any .NET version (9, 10) independent of the host. This guide is the 2026 setup: Flex Consumption + isolated .NET 9 + managed identity + triggers that scale on real signals.
- Your function uses
FUNCTIONS_WORKER_RUNTIME=dotnet(in-process β deprecated) - You moved to Premium just to get out of cold starts and now you're paying for idle
- You can't explain why a function with a Service Bus trigger only scales to 1 instance
- You're still wiring connection strings instead of using identity-based connections
This is the modern Functions setup β Flex Consumption, isolated worker, identity everywhere, and concurrency tuned to the trigger.
Concept Explanation
Hosting plans in 2026:
- Flex Consumptionβ pay-per-execution, fast cold starts (sub-second on .NET), per-instance concurrency knobs, VNet integration, instance memory choice (2 GB / 4 GB), "always-ready" instance pool. The default.
- Premium (EP) β pre-warmed instances, no cold start, longer max duration, durable functions at scale. Pick when you need > 60 min duration or always-warm at high QPS.
- Consumption (legacy)β original serverless tier. Use only if Flex Consumption isn't in your region.
- App Service Plan β runs on the same VMs as a Web App. Pick when you already have spare plan capacity.
flowchart LR
HTTP["HTTP Request"] --> FN1["Function: ProcessOrder<br/>(httpTrigger)"]
SB["Service Bus Queue<br/>(orders)"] -->|push, scale 0β200| FN2["Function: HandleOrder<br/>(serviceBusTrigger)"]
BLOB["Blob: uploads/*"] -->|Event Grid| FN3["Function: Resize<br/>(blobTrigger)"]
TIMER["CRON 0 */5 * * * *"] --> FN4["Function: SyncStatus<br/>(timerTrigger)"]
FN1 --> KV["Key Vault<br/>(MI)"]
FN2 --> SQL["Azure SQL<br/>(MI)"]
FN3 --> CV["Azure AI Vision"]
style FN1 fill:#0078D4,color:#fff,stroke:#005a9e
style FN2 fill:#0078D4,color:#fff,stroke:#005a9e
style FN3 fill:#0078D4,color:#fff,stroke:#005a9e
style FN4 fill:#0078D4,color:#fff,stroke:#005a9e
Implementation
Step 1: Provision Flex Consumption with Bicep
param appName string
param location string = resourceGroup().location
param storageAccountName string
resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
name: '${appName}-plan'
location: location
sku: { tier: 'FlexConsumption', name: 'FC1' }
kind: 'functionapp,linux'
properties: { reserved: true }
}
resource fn 'Microsoft.Web/sites@2023-12-01' = {
name: appName
location: location
kind: 'functionapp,linux'
identity: { type: 'SystemAssigned' }
properties: {
serverFarmId: plan.id
httpsOnly: true
functionAppConfig: {
deployment: {
storage: {
type: 'blobContainer'
value: 'https://${storageAccountName}.blob.core.windows.net/deploymentpackage'
authentication: { type: 'SystemAssignedIdentity' }
}
}
scaleAndConcurrency: {
maximumInstanceCount: 100
instanceMemoryMB: 2048
}
runtime: { name: 'dotnet-isolated', version: '9.0' }
}
siteConfig: {
appSettings: [
// Identity-based connection β no key in storage conn string!
{ name: 'AzureWebJobsStorage__accountName', value: storageAccountName }
{ name: 'AzureWebJobsStorage__credential', value: 'managedidentity' }
]
}
}
}Step 2: Isolated worker (.NET 9) β Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Azure.Identity;
var host = new HostBuilder()
.ConfigureFunctionsWebApplication() // ASP.NET Core integration
.ConfigureServices(services =>
{
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
services.AddSingleton(new TokenCredential[] { new DefaultAzureCredential() }[0]);
})
.Build();
await host.RunAsync();Step 3: HTTP trigger (Functions in 2026 use ASP.NET Core integration)
public class OrdersFunction(ILogger<OrdersFunction> log)
{
[Function("ProcessOrder")]
public async Task<IResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req)
{
var order = await req.ReadFromJsonAsync<Order>();
if (order is null) return Results.BadRequest();
log.LogInformation("Processing order {Id}", order.Id);
return Results.Ok(new { accepted = true, order.Id });
}
}Step 4: Service Bus trigger β let the platform scale you
public class OrderHandler(ILogger<OrderHandler> log)
{
// Fully qualified namespace + identity β no connection string
[Function("HandleOrder")]
public async Task Run(
[ServiceBusTrigger("orders", Connection = "ServiceBus")] ServiceBusReceivedMessage msg,
ServiceBusMessageActions actions)
{
try
{
var order = JsonSerializer.Deserialize<Order>(msg.Body)!;
await ProcessAsync(order);
await actions.CompleteMessageAsync(msg);
}
catch (PoisonMessageException ex)
{
await actions.DeadLetterMessageAsync(msg, "Poison", ex.Message);
}
}
}// host.json β concurrency that actually fits Service Bus
{
"version": "2.0",
"extensions": {
"serviceBus": {
"maxConcurrentCalls": 32,
"maxConcurrentSessions": 8,
"prefetchCount": 0
}
}
}# App settings (identity-based)
ServiceBus__fullyQualifiedNamespace = myns.servicebus.windows.net
ServiceBus__credential = managedidentityStep 5: Blob trigger β via Event Grid (the only way to scale at volume)
// Default polling-based blob trigger doesn't scale past a few thousand blobs/min.
// Event-based trigger uses Event Grid β Functions and scales horizontally.
public class ResizeFunction
{
[Function("Resize")]
public async Task Run(
[BlobTrigger("uploads/{name}", Source = BlobTriggerSource.EventGrid,
Connection = "AzureWebJobsStorage")] Stream blob,
string name)
{
await image.ResizeAsync(blob, name);
}
}Step 6: Deploy with az CLI (run-from-package style)
# zip the publish output
dotnet publish -c Release -o ./out
(cd out && zip -r ../app.zip .)
# upload to the deployment storage container (auth via current az identity)
az storage blob upload \
--account-name myfnstorage --container-name deploymentpackage \
--name app.zip --file app.zip --overwrite --auth-mode login
# trigger sync β Flex Consumption picks up the new package
az functionapp deployment source config-zip \
-g rg-fn -n my-fn-app --src app.zip --build-remote falsePitfalls
1. Still on the in-process .NET model. It's end-of-life. Migrate to dotnet-isolated; the project file isFunctionsWorker.Sdk and the entry point is Program.cs. New language version targeting requires it.
2. Default polling Blob trigger at scale.The classic blob trigger polls. At more than a few thousand new blobs/minute you'll miss events. Always use Source = BlobTriggerSource.EventGrid for production blob workflows.
3. Connection strings in AzureWebJobsStorage. Use the identity-based form (__accountName + __credential=managedidentity). Same for Service Bus, Event Hubs, Cosmos. Grants needed: Storage Blob Data Owner, Service Bus Data Receiver, etc.
4. Wrong concurrency knob. Functions has two scale dials: instance count (platform) and per-instance concurrency(you, in host.json). On Flex Consumption you also tune maximumInstanceCount and HTTP per-instance concurrency inscaleAndConcurrency. Misconfigure and you either over-saturate downstream or never scale out.
5. Long-running HTTP functions.Flex Consumption's effective max execution is bounded; HTTP requests time out at the gateway after 230 seconds regardless. For long work, queue + Durable Functions or off-load to a worker.
6. Cold start blamed for everything.A modern .NET 9 isolated app on Flex Consumption cold-starts in well under a second. If you're seeing seconds, check JIT, large dependency graphs, or expensive startup code (DB pings, HTTP probes). Use ReadyToRun and trim startup work.
Practical Takeaways
- Flex Consumption + .NET isolated 9 (or 10) is the 2026 default. Move off in-process and legacy Consumption.
- Identity-based connections everywhere β Storage, Service Bus, Event Hubs, Cosmos. No keys.
- Blob workflows: always Event Gridβsourced trigger. Polling doesn't scale.
- Tune both axes of concurrency:
maximumInstanceCount+ per-trigger concurrency inhost.json. - Use Durable Functions for orchestrations, fan-out/fan-in, and human-in-the-loop. Don't hand-roll state.
- Deploy via blob package + identity β zip up, upload, sync. No FTP, no Kudu push.
- Monitor with Application Insights from day one. The Functions worker integration is built in.

