DEV Community

Cover image for Production Realities: When Azure Functions Stops Being Serverless
Martin Oehlert
Martin Oehlert

Posted on

Production Realities: When Azure Functions Stops Being Serverless

Azure Functions for .NET Developers: Series


The Enterprise Reality Check

At what point does an Azure Functions deployment stop being serverless and start being managed compute with a monthly bill? The shift happens not in one decision but in a sequence of reasonable ones: a VNet requirement from the security team, then private endpoints for the storage account, then an API gateway because the function is public-facing.

Your function works on Consumption. Zero cost at idle, automatic scaling, no infrastructure to think about. Then the security review lands. VNet integration is mandatory. Consumption doesn't support VNets, so you move to Flex Consumption. Private endpoints? Flex handles those too. You're still paying close to nothing at idle.

Then the surrounding infrastructure arrives. API Management adds $147 to $700. WAF protection adds $333. Each requirement passes its own cost-benefit test. None of them would make you question the architecture on their own. But the total floor lands somewhere between $530 and $1,080 per month, and the function plan itself is the smallest line item on the invoice.

The serverless pitch from Part 1 of this series was real. It just applies to a narrower set of workloads than most teams expect when they start. Once you're past that boundary, the question isn't whether to pay more. It's whether the Functions abstraction is still worth paying for, or whether Container Apps or App Service would give you the same outcome with less friction.

VNet: The Requirement That Changes Everything

Most enterprise environments mandate VNet integration for anything touching internal databases, key vaults behind private endpoints, or services that shouldn't be exposed to the public internet. The Consumption plan doesn't support VNet. That single requirement forces you into a different hosting plan, and each plan carries different pricing and operational constraints.

These are your options (East US pricing, April 2026):

Plan VNet Scale to Zero Min Idle Cost/mo
Consumption No Yes $0
Flex Consumption (on-demand) Yes Yes $0
Flex Consumption (1 always-ready, 2 GB) Yes No ~$21
Premium EP1 Yes No ~$146
Premium EP2 Yes No ~$291
Dedicated S1/P1v3 Yes No varies
Container Apps (1 replica, 0.25 vCPU) Yes Yes ~$10

The jump from Consumption to Premium EP1 is $146/month for a single function app sitting idle. That's the cost of VNet access before your code processes a single request. Premium EP2 doubles it. These aren't theoretical numbers: they're the minimum monthly charges while your function waits for traffic.

Flex Consumption went GA in November 2024, and Microsoft now positions it as the recommended path for apps that need dynamic scaling with VNet support. In on-demand mode, Flex preserves the scale-to-zero model that made Consumption attractive. It also skips the Azure Files dependency (the shared file system Premium uses for deployment artifacts and runtime state). If your security team mandates private networking for storage, that saves roughly $30/month on private endpoint costs you'd otherwise pay on Premium. Under the hood, Flex uses shared gateways (up to 27 shared gateway IPs) instead of dedicated VNet-injected workers. That's how it keeps costs lower.

If you need guaranteed warm instances to avoid cold starts, Flex's always-ready configuration starts at about $21/month for one instance with 2 GB memory. That's still a fraction of Premium EP1.

But Flex has real constraints. Before you commit to it, compare what you're giving up:

Constraint Flex Consumption Premium
OS Linux only Windows + Linux
Apps per plan One Multiple
Deployment slots Not supported Up to 3
In-process .NET Not supported Supported
App init timeout Fixed 30s No limit
NFS file shares No Yes
Regional availability Limited Broad

The one-app-per-plan limitation is easy to overlook. You can't consolidate multiple function apps onto a single Flex plan the way you would with Premium. For teams running five or ten function apps, Premium's ability to share a single plan across all of them can actually cost less per app than running each on its own Flex instance.

The 30-second app init timeout is fixed. Not configurable. If your function app loads large dependency injection containers and connects to multiple databases at startup, 30 seconds may not be enough. Premium has no startup timeout limit, so heavy initialization is never a problem there.

If your codebase uses in-process .NET (the older hosting model where your function runs inside the Functions host process), Flex doesn't support it. You'd need to migrate to the isolated worker model first, which is its own project.

If you need Windows, deployment slots, or in-process .NET: Premium is your only option. If you're on Linux with the isolated worker model and can live with one app per plan, Flex Consumption gives you VNet support without abandoning scale-to-zero.

One more thing worth knowing: Linux Consumption is on a deprecation path. No new features after September 2025, with retirement scheduled for September 2028. Microsoft is pushing new workloads toward Flex Consumption, and the deprecation timeline makes that push harder to ignore.

The Plan Escalation Path

You start on Consumption. Your function triggers on an HTTP request, processes a message, writes to Cosmos DB. It costs nothing when idle. The serverless model, doing what it's supposed to do.

Then the requirements start arriving, one at a time.

VNet integration

Your security team requires all compute to run inside a virtual network. Consumption doesn't support VNet integration, so you move to Flex Consumption (on-demand only). This still scales to zero. You're still paying nothing at idle. No problem.

Running total: $0/mo idle

Private endpoints

Next review: inbound traffic to your function app must go through a private endpoint, and the backing storage accounts need private endpoints too.

Flex Consumption supports inbound private endpoints. You don't need to leave Flex for this. Your function app keeps scale-to-zero, and the private endpoint adds ~$7/month.

Flex also needs private endpoints for its backing storage accounts: Blob, Queue, and Table. Three endpoints, not four, because Flex has no Azure Files dependency. That's ~$22/mo for storage endpoints.

One deployment gotcha worth knowing: combining VNet integration with inbound private endpoints on Flex can cause deployment timeouts at the Kudu RemoveWorkersStep. The current workaround is temporarily removing the private endpoint during deployments, then re-adding it. Not ideal for automated pipelines, and worth factoring into your CI/CD design.

Running total: ~$29/mo (Flex on-demand)

The fork: when Premium becomes unavoidable

Most teams can stay on Flex through the VNet and private endpoint requirements. But Flex has constraints that force some teams onto Premium EP1:

  • Windows hosting required
  • Deployment slots for blue-green deployments
  • In-process .NET (not yet migrated to isolated worker)
  • Multiple function apps sharing a single plan
  • App init exceeding 30 seconds (Flex's hard timeout)

If any of these apply, EP1 gives you 1 vCPU and 3.5 GB of memory. The math: 1 vCPU at $116.80 plus 3.5 GB at $8.322 per GB = ~$146/mo. It runs 24/7 whether your function executes or not. Storage private endpoints on Premium cost ~$30/mo (four endpoints, including Azure Files).

Running total if forced to Premium: ~$176/mo

Cold starts

On Flex, cold starts are still possible when scaling from zero. If your workload needs guaranteed warm instances, Flex's always-ready configuration starts at ~$21/month for one instance with 2 GB memory. On Premium, cold starts are a non-issue: EP1 keeps at least one instance warm by default.

Running total: ~$50/mo (Flex + always-ready) or ~$176/mo (Premium)

API Management

Your API needs rate limiting and a developer portal. You add Azure API Management. The pricing depends on what your organization needs:

  • APIM Basic (classic): ~$147/mo, no VNet integration, 99.95% SLA
  • APIM Standard v2: ~$700/mo, partial VNet support (backend only), 99.95% SLA
  • APIM Premium (classic): ~$2,795/mo, full VNet integration, 99.99% SLA

Most teams start with Basic and accept the VNet gap. Some compliance requirements force Standard v2 or higher.

Running total: ~$197/mo (Flex + Basic) or ~$323/mo (Premium + Basic)

WAF protection

Compliance also wants a Web Application Firewall in front of your API. You deploy Application Gateway WAF_v2.

The breakdown: $0.443/hour for 730 hours ($323), plus at least one capacity unit ($10.50), plus a public IP ($3.65). That's ~$333-335/mo.

Application Gateway v1 retires April 28, 2026, so WAF_v2 is the only option going forward.

Running total: ~$530/mo (Flex floor) or ~$656/mo (Premium floor)

The full picture

Consumption ($0 idle)
  + VNet requirement
  → Flex on-demand: still $0 idle

  + private endpoints (inbound + storage)
  → Flex: ~$29/mo (1 inbound PE + 3 storage PEs)
  → Premium (if forced by constraints): ~$176/mo (EP1 + 4 storage PEs)

  + cold start elimination
  → Flex always-ready: ~$21/mo
  → Premium: included (always-on)

  + API Management
  → APIM Basic: ~$147/mo
  → OR APIM Standard v2: ~$700/mo

  + WAF protection
  → Application Gateway WAF_v2: ~$333/mo

  Flex path:
  = ~$530/mo floor (Flex + always-ready + PEs + APIM Basic + WAF)
  = ~$1,083/mo ceiling (Flex + PEs + APIM Standard v2 + WAF)

  Premium path (forced by constraints):
  = ~$656/mo floor (EP1 + PEs + APIM Basic + WAF)
  = ~$1,209/mo ceiling (EP1 + PEs + APIM Standard v2 + WAF)
Enter fullscreen mode Exit fullscreen mode

Every one of these requirements is reasonable on its own. Your security team isn't wrong to ask for VNet integration. Private endpoints are a real protection. APIM and WAF exist because APIs need them.

The function plan itself is the smallest factor. On Flex, your compute cost at idle is $0 to $50/month. On Premium, it's $146 to $176. Either way, APIM and WAF add $480 to $1,033 on top. Those two services dominate the bill regardless of which Functions plan you choose.

Build and Deploy Friction

The cost story from the plan escalation section is the monthly bill. The deployment story is the engineering time you spend before your code even runs.

You cannot convert a function app from one plan to another in place. Moving from Consumption to Flex Consumption, or from Premium to Flex, means creating a new function app, redeploying your code, and deleting the old one. There is no az functionapp update --sku FC1. Microsoft's own migration guide recommends running both apps in parallel during a transition period, then cutting over. For production workloads, that's a blue-green deployment you didn't plan for.

The app settings cleanup

Flex Consumption deprecates roughly 20 app settings and site properties that other plans rely on. If you copy your existing configuration to a new Flex app without cleaning it up, the deployment fails or the app behaves unpredictably.

These settings must be removed:

# Deployment (handled by functionAppConfig.deployment.storage on Flex)
WEBSITE_RUN_FROM_PACKAGE

# Azure Files (Flex has no Azure Files dependency)
WEBSITE_CONTENTAZUREFILECONNECTIONSTRING
WEBSITE_CONTENTSHARE
WEBSITE_SKIP_CONTENTSHARE_VALIDATION

# Networking (inherited from the integrated VNet on Flex)
WEBSITE_CONTENTOVERVNET
WEBSITE_VNET_ROUTE_ALL
WEBSITE_DNS_SERVER

# Runtime (managed via functionAppConfig.runtime on Flex)
FUNCTIONS_EXTENSION_VERSION
FUNCTIONS_WORKER_RUNTIME

# Scaling (renamed in functionAppConfig.scaleAndConcurrency)
WEBSITE_MAX_DYNAMIC_APPLICATION_SCALE_OUT
Enter fullscreen mode Exit fullscreen mode

The most dangerous one is WEBSITE_RUN_FROM_PACKAGE. On Consumption and Premium, this setting controls how your code gets deployed. On Flex, it must not exist. Flex uses functionAppConfig.deployment.storage to point at a blob container instead of Azure Files. If WEBSITE_RUN_FROM_PACKAGE is still present, the deployment silently uses the wrong mechanism.

Site properties change too. alwaysOn must be false on Flex (it's invalid), but true on Premium and Dedicated. functionsRuntimeScaleMonitoringEnabled is unnecessary on Flex because scale monitoring is built in, but forgetting to remove it won't break anything. ARM template properties like linuxFxVersion, containerSize, and isReserved are all replaced by the functionAppConfig section.

Infrastructure as Code breaks across plans

Your Terraform and Bicep templates don't just need new property values. They need different resources entirely.

In Terraform, azurerm_linux_function_app does not work with the Flex Consumption SKU. Attempting to provision it with an FC1 service plan fails. You need azurerm_function_app_flex_consumption, a separate resource introduced in AzureRM provider v4.21.0:

# Consumption / Premium: this resource
resource "azurerm_linux_function_app" "func" {
  service_plan_id = azurerm_service_plan.plan.id
  # ...
}

# Flex Consumption: different resource, different schema
resource "azurerm_function_app_flex_consumption" "func" {
  service_plan_id = azurerm_service_plan.plan.id

  site_config {}

  storage_container_type    = "blobContainer"
  storage_container_endpoint = "${azurerm_storage_account.sa.primary_blob_endpoint}${azurerm_storage_container.deploy.name}"
  # ...
}
Enter fullscreen mode Exit fullscreen mode

The Flex resource requires a blob storage container for deployments (no Azure Files), supports maximum_instance_count and instance_memory_mb properties that don't exist on the standard resource, and has its own quirks. As of early 2026, you still need to set AzureWebJobsStorage to an empty string as a workaround when using managed identity authentication, then use AzureWebJobsStorage__accountName for the actual connection.

In Bicep, the SKU values map to different tiers:

Plan SKU Name SKU Tier
Consumption Y1 Dynamic
Flex Consumption FC1 FlexConsumption
Premium EP1/EP2/EP3 ElasticPremium

The FC1 plan also requires reserved: true and a functionAppConfig section that replaces most of the properties you'd normally set as app settings. That's a structural rewrite of your deployment template, not a property change.

CI/CD pipeline adjustments

GitHub Actions requires the sku parameter in azure/functions-action when deploying to Flex Consumption with a publish profile:

- uses: Azure/functions-action@v1
  with:
    app-name: ${{ env.FUNCTION_APP_NAME }}
    package: ${{ env.PACKAGE_PATH }}
    publish-profile: ${{ secrets.PUBLISH_PROFILE }}
    sku: 'flexconsumption'
    remote-build: 'true'
Enter fullscreen mode Exit fullscreen mode

Without sku: 'flexconsumption', the action deploys using the standard Consumption mechanism, which fails silently or produces a broken deployment. With OIDC or service principal authentication, the action can auto-detect the SKU, but publish profile deployments need it explicitly.

The scm-do-build-during-deployment and enable-oryx-build flags that you might have in your existing workflow are also wrong for Flex. Flex always performs an Oryx build during remote deployment. Setting those flags manually can interfere with the process.

Private endpoints break GitHub-hosted runners

If your function app runs on Premium with private endpoints enabled, the SCM/Kudu site is not publicly reachable. GitHub-hosted runners cannot connect to it. Your deployment fails with Failed to fetch Kudu App Settings (CODE: 404) or a 401, and the error message gives you almost no indication that networking is the problem.

Your options:

  • Self-hosted runner inside the VNet: works, but now you're maintaining a VM ($50-100/month) to deploy a "serverless" function
  • GitHub-hosted runners with Azure private networking: GitHub can inject a runner NIC directly into your VNet subnet, giving hosted runners private access without self-hosted infrastructure. Requires a GitHub Team or Enterprise Cloud plan and larger runners (2-64 vCPU, per-minute billing). Supported in 25 Azure regions as of early 2026, but notably not West Europe.
  • Deploy via ARM using a service principal: bypasses SCM entirely, pushes configuration through the Azure Resource Manager API
  • Stage to blob storage: upload your package to a storage account the function app can reach, then trigger deployment from there

On Premium, you also need WEBSITE_SKIP_CONTENTSHARE_VALIDATION=1 in your ARM or Bicep templates when the backing storage account has a firewall or private endpoints. Without it, the ARM deployment fails during content share validation because the deployment engine can't reach the storage account through the private endpoint.

The compound effect

Any one of these issues is a half-day fix. The compound effect is what costs real engineering time: you change plans, which changes your Terraform resources, which changes your app settings, which changes your GitHub Actions workflow, which breaks because of private endpoint networking. Each layer has its own failure mode, its own error messages, and its own documentation scattered across different Microsoft Learn pages.

The real cost of plan migration shows up in the sprint consumed by infrastructure work, not in the Azure bill.

When Serverless Stops Making Sense

At some point, the friction outweighs the abstraction. If you're paying $530/month or more, fighting plan migrations in Terraform, and managing deployment workarounds because private endpoints interfere with your CI pipeline, you should be asking: is the Functions hosting model still earning its keep?

Signs it's time to look elsewhere:

  • Your total infrastructure cost exceeds what the "serverless" label saves you in operational effort
  • You're spending more time working around platform constraints than building features
  • Your build and deploy pipeline is already as complex as it would be with containers
  • Your team needs operational control (sidecars, traffic splitting, custom health probes) that Functions doesn't expose

The two alternatives worth evaluating are Azure Container Apps and App Service. They solve different problems.

Azure Container Apps: the container-native path

Azure Container Apps (ACA) can host the Functions runtime directly. The v2 model, which Microsoft recommends for all new deployments, creates a single Microsoft.App resource with kind=functionapp. No hidden proxy resources, no dual-resource management. Your function app is a container app with Functions triggers and bindings wired in.

The resource definition looks like a normal container app deployment:

az containerapp create \
  --name my-func-app \
  --resource-group rg-prod \
  --environment my-aca-env \
  --image myregistry.azurecr.io/my-func:latest \
  --kind functionapp \
  --min-replicas 0 \
  --max-replicas 10
Enter fullscreen mode Exit fullscreen mode

KEDA (Kubernetes-based Event Driven Autoscaling) handles scaling. The Functions runtime automatically configures KEDA scale rules based on your triggers. You don't write KEDA definitions yourself; the platform infers them from your bindings. HTTP, Service Bus, Event Hubs, Queue Storage, and other triggers all map to KEDA scalers behind the scenes, and your app can scale to zero when idle.

What you gain over Premium Functions:

  • Cost: a single replica at 0.25 vCPU / 512 MB idles at roughly ~$10/month on the Consumption workload profile. Compare that to Premium EP1 at ~$146/month. With scale-to-zero, idle cost drops to $0.
  • Sidecar containers: run log forwarders, auth proxies, or Dapr sidecars alongside your function app in the same pod
  • Dapr integration: pub/sub, state management, and service invocation without managing the infrastructure
  • Traffic splitting via revisions: route a percentage of traffic to a new version before promoting it, something Functions deployment slots can't do with the same granularity
  • GPU support: if you're running inference workloads alongside event-driven functions, ACA supports GPU-backed workload profiles

What you give up:

  • Containerization is mandatory. There is no code-only deployment path. You build a Docker image, push it to a registry, and deploy from there. If your team has no container experience, this is a real adoption cost.
  • No built-in continuous deployment from the Functions tooling. You wire up GitHub Actions or Azure Pipelines yourself.
  • Inbound Private Endpoints through the Functions networking layer are not available. The Functions networking features table on Microsoft's docs shows a blank cell for "Inbound Private Endpoints" under the Container Apps column. ACA itself supports private endpoints at the environment level (workload profiles environments only), but the Functions-specific private endpoint feature does not carry over. If your compliance requirements specifically mandate Functions inbound private endpoints, Flex Consumption or Premium are your options.

The environment-level private endpoint on ACA carries additional charges through the Dedicated Plan Management fee. Budget roughly $67-70/month for this capability, which applies at the environment level regardless of how many apps you run inside it.

App Service: the predictable option

App Service doesn't get much attention in the serverless conversation, but it's worth considering if your workload has predictable traffic and you've already left the scale-to-zero model behind. On Premium EP1, you're paying ~$146/month for a single function app that never scales to zero anyway. An App Service P1v3 plan gives you 2 vCPUs and 8 GB of memory. It supports multiple apps on the same plan, full deployment slots (up to 5 on Standard, 20 on Premium), and no cold starts. The pricing is comparable, and you get operational features that Functions on Premium doesn't match.

App Service won't give you KEDA-based event scaling or scale-to-zero. It's a fixed-compute model. But if you're already paying for always-on compute through Premium Functions, the question is whether the Functions event-driven abstractions justify the constraints that come with them.

Picking the right exit

The choice depends on what pushed you away from Functions in the first place:

If your main frustration is cost, Container Apps on the Consumption workload profile gives you scale-to-zero with VNet support at a fraction of Premium pricing. You keep the Functions programming model, triggers, and bindings.

If your frustration is operational control, Container Apps gives you sidecars, revisions, and a container runtime you can customize. The trade-off is containerization overhead and the requirement to build and manage container images.

Teams frustrated by complexity for a steady workload often find that App Service is the right fit. It strips away the serverless machinery and gives you a predictable compute environment with mature deployment tooling.

None of these are a universal upgrade. Each one trades a Functions limitation for a different set of constraints. The point is to make that trade consciously, not to discover it after migration.

Making an Honest Choice

Three plans, three different products.

Consumption is genuine serverless. Your code runs, you pay for execution time, it scales to zero when idle. If your workload is public-facing, doesn't need VNet access, and won't face a security review that mandates private networking, Consumption is the right plan. It does exactly what the marketing says. The catch: Linux Consumption enters restricted feature mode in September 2025, with full retirement in September 2028. New workloads shouldn't start here.

Flex Consumption is serverless with enterprise networking. It went GA in November 2024 and it's the plan Microsoft recommends for new dynamic-scale workloads in 2026. You get VNet integration, inbound private endpoints, scale-to-zero, and no Azure Files dependency. The constraints are real (Linux only, one app per plan, no deployment slots, 30-second init timeout), but for teams that can work within them, Flex keeps the serverless economics intact while passing a security review.

Premium is managed compute with event-driven scaling. It is not serverless. You're paying for always-on instances whether traffic arrives or not. Premium exists because some requirements (Windows, deployment slots, in-process .NET, multiple apps per plan) have no other home. If you're on Premium, own that decision. Budget for it as compute, not as serverless with extra features.

The distinction matters most at the moment you least want to think about it: during the security review, when someone asks why your function app can't reach a private endpoint. Know which plan you're actually buying before that conversation starts. Migrating between plans means deleting and recreating the function app, updating Terraform resources, and rewriting deployment pipelines. It's not a configuration change. It's a project.

Has a security review pushed you from Consumption to Premium, or did you start on Premium from day one?


Azure Functions for .NET Developers: Series

Top comments (0)