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.

πŸ€” Sound familiar?
  • 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              = managedidentity

Step 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 false

Pitfalls

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 in host.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.