<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Martin Oehlert</title>
    <description>The latest articles on DEV Community by Martin Oehlert (@martin_oehlert).</description>
    <link>https://dev.to/martin_oehlert</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1661015%2Fd0bdf508-0244-49d8-8655-aea054d71b86.png</url>
      <title>DEV Community: Martin Oehlert</title>
      <link>https://dev.to/martin_oehlert</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/martin_oehlert"/>
    <language>en</language>
    <item>
      <title>Scaling Azure Functions: Consumption vs Premium vs Dedicated</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 01 May 2026 03:45:15 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/scaling-azure-functions-consumption-vs-premium-vs-dedicated-2gm</link>
      <guid>https://dev.to/martin_oehlert/scaling-azure-functions-consumption-vs-premium-vs-dedicated-2gm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions Beyond the Basics&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Continues from &lt;a href="https://dev.to/martin_oehlert/series/38960"&gt;Azure Functions for .NET Developers&lt;/a&gt; (Parts 1-9)&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/running-azure-functions-in-docker-why-and-how-1hal"&gt;Running Azure Functions in Docker: Why and How&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/docker-pitfalls-i-hit-and-how-to-avoid-them-2395"&gt;Docker Pitfalls I Hit (And How to Avoid Them)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3: Scaling Azure Functions: Consumption vs Premium vs Dedicated&lt;/strong&gt; &lt;em&gt;(you are here)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your Consumption plan function works fine in dev. Then production traffic arrives, the app scales to zero during a quiet period, and the next request takes 6.8 seconds. The question that follows is always the same: do you switch to Premium at $146/month, or is there something between free-with-cold-starts and always-warm-but-always-billing? Azure Functions has five hosting options now (Consumption, Flex Consumption, Premium, Dedicated, and Container Apps), each with a different billing model and a different answer to that question. This article covers the four App Service-based plans; Container Apps is a different deployment model aimed at containerized microservices. All code samples are in the &lt;a href="https://github.com/MO2k4/azure-functions-samples/tree/main/ScalingDemo" rel="noopener noreferrer"&gt;companion repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consumption: true serverless, true cold starts
&lt;/h2&gt;

&lt;p&gt;Microsoft now labels the Consumption plan as "legacy" in its hosting docs and is directing new serverless workloads to Flex Consumption. But Consumption is still where most Functions apps start, and where many should stay. You deploy your code, the platform handles the rest. No servers to manage, no capacity to plan. You pay only when your functions execute.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the scale controller works
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;scale controller&lt;/strong&gt; monitors event rates for each trigger type and decides how many instances to run. Since runtime v4.19.0, it uses &lt;strong&gt;target-based scaling&lt;/strong&gt; by default. The formula is one division:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;desired instances = event source length / target executions per instance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What "event source length" means depends on the trigger. For Storage Queues, it's queue length. For Service Bus, active message count. For Event Hubs, unprocessed events per partition. For Cosmos DB, pending changes in the change feed. The controller reads these signals and adjusts instance count accordingly.&lt;/p&gt;

&lt;p&gt;The controller adds up to four instances at a time. HTTP triggers get new instances at most once per second. Non-HTTP triggers scale at most once every 30 seconds. This is fast enough for gradual traffic ramps but won't help with sudden spikes from zero.&lt;/p&gt;

&lt;h3&gt;
  
  
  Instance limits and billing
&lt;/h3&gt;

&lt;p&gt;Each Consumption instance gets 1.5 GB of memory and one CPU core. The maximum instance count is &lt;strong&gt;200 on Windows&lt;/strong&gt; and &lt;strong&gt;100 on Linux&lt;/strong&gt; (with a 500-instance-per-subscription-per-hour rate limit on Linux).&lt;/p&gt;

&lt;p&gt;Billing has two components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Executions&lt;/strong&gt;: $0.20 per million, with 1,000,000 free per month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution time&lt;/strong&gt;: $0.000016 per GB-second, with 400,000 GB-seconds free per month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Memory is rounded up to the nearest 128 MB bucket. Execution time rounds to the nearest millisecond, with a minimum billable unit of 128 MB x 100 ms. For a function that runs a few thousand times a day at under a second each, you'll stay well inside the free grant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cold start reality on .NET
&lt;/h3&gt;

&lt;p&gt;After roughly 20 minutes of inactivity, the Consumption plan scales to zero. The next request waits for the platform to provision a fresh instance and start your application from scratch.&lt;/p&gt;

&lt;p&gt;On .NET isolated worker, that cold start typically lands between &lt;strong&gt;2 and 7 seconds&lt;/strong&gt;. Heavy DI registrations push it past 10. The in-process model was faster, but Microsoft is &lt;a href="https://learn.microsoft.com/en-us/azure/azure-functions/migrate-dotnet-to-isolated-model" rel="noopener noreferrer"&gt;retiring it in November 2026&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For timer triggers, queue processors, and other background work, a few seconds of cold start is invisible. For HTTP endpoints that a user is waiting on, it's a problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Consumption can't do
&lt;/h3&gt;

&lt;p&gt;The hard constraints that push teams to other plans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No VNet integration.&lt;/strong&gt; If your function needs to reach resources inside a virtual network, Consumption is off the table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10-minute execution timeout.&lt;/strong&gt; The default is 5 minutes, configurable to 10. Long-running orchestrations or batch jobs need a different plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No per-function scaling.&lt;/strong&gt; All functions in the app scale together. A chatty timer trigger can cause the platform to allocate instances that your HTTP trigger didn't need.
&amp;lt;!-- Outbound connection limit sourced from Azure Functions networking docs and SNAT port limits. Verify against current docs before publish. --&amp;gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;600 active outbound connections per instance.&lt;/strong&gt; Hit this with parallel HTTP calls to external APIs and requests start failing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Linux Consumption is retiring September 30, 2028.&lt;/strong&gt; Microsoft is directing all new Linux serverless workloads to Flex Consumption. If you're starting a new project on Linux, skip Consumption entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flex Consumption: the middle ground
&lt;/h2&gt;

&lt;p&gt;Flex Consumption is the plan Microsoft now recommends for new serverless workloads. It addresses the two biggest Consumption limitations: no VNet support and no way to reduce cold starts without jumping to a $146/month Premium plan.&lt;/p&gt;

&lt;p&gt;The plan scales to zero like Consumption, but adds &lt;strong&gt;always-ready instances&lt;/strong&gt; that you can configure to stay warm. It supports &lt;strong&gt;VNet integration&lt;/strong&gt; out of the box. And it scales to &lt;strong&gt;1,000 instances&lt;/strong&gt; instead of Consumption's 200.&lt;/p&gt;

&lt;h3&gt;
  
  
  Always-ready instances vs on-demand
&lt;/h3&gt;

&lt;p&gt;By default, Flex Consumption behaves like regular Consumption: zero instances when idle, on-demand instances when events arrive. The difference is you can configure &lt;strong&gt;always-ready instances&lt;/strong&gt; that stay running regardless of traffic.&lt;/p&gt;

&lt;p&gt;Always-ready instances are assigned to &lt;strong&gt;scale groups&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;http&lt;/code&gt;: all HTTP and SignalR triggers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;durable&lt;/code&gt;: orchestration, activity, and entity triggers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;blob&lt;/code&gt;: Event Grid-based blob triggers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;function:&amp;lt;FUNCTION_NAME&amp;gt;&lt;/code&gt;: a specific function&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Setting always-ready to 2 for the &lt;code&gt;http&lt;/code&gt; group keeps two instances permanently running for HTTP functions. Those handle traffic first. If demand exceeds their capacity, the platform adds on-demand instances on top.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az functionapp scale config &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; my-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-func-app &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--always-ready&lt;/span&gt; &lt;span class="nv"&gt;http&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On-demand instances scale to zero when idle. Always-ready instances are billed continuously whether they're executing functions or not. If you enable zone redundancy, the minimum is 2 always-ready instances per group.&lt;/p&gt;

&lt;h3&gt;
  
  
  Billing: per-second, not per-execution
&lt;/h3&gt;

&lt;p&gt;Flex Consumption bills differently from Consumption. Instead of per-execution pricing with sampled memory, you choose a &lt;strong&gt;fixed instance size&lt;/strong&gt; upfront and pay per GB-second of active execution time:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzlhd0ijfizkni34vn4mv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzlhd0ijfizkni34vn4mv.png" alt="Flex Consumption instance sizes and HTTP concurrency" width="484" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On-demand rates are $0.000026 per GB-second and $0.40 per million executions. The monthly free grant is smaller than Consumption: 250,000 executions and 100,000 GB-seconds (compared to Consumption's 1,000,000 and 400,000).&lt;/p&gt;

&lt;p&gt;Always-ready instances have a separate billing structure with no free grant. The baseline (idle) rate is $0.000004 per GB-second, roughly 6.5x cheaper than the on-demand execution rate. When always-ready instances are actively executing, the execution time rate is $0.000016 per GB-second (the same as Consumption's rate, and cheaper than on-demand).&lt;/p&gt;

&lt;p&gt;The minimum billable execution is 1,000 ms (1 second). After that, billing rounds to the nearest 100 ms. This is less granular than Consumption's per-millisecond rounding, so very fast functions (under 100 ms) cost relatively more on Flex.&lt;/p&gt;

&lt;p&gt;Each instance also gets an extra 272 MB platform buffer that isn't billed. This is memory the Functions host and worker process use, not your function code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scale behavior
&lt;/h3&gt;

&lt;p&gt;Flex Consumption scales per function by trigger type. HTTP and SignalR triggers scale together. Durable Functions triggers scale together. Blob triggers (Event Grid source) scale together. Everything else scales independently per function. This fixes a real problem from Consumption, where a noisy timer trigger could cause unnecessary instance allocation for your HTTP functions.&lt;/p&gt;

&lt;p&gt;Maximum instances: &lt;strong&gt;1,000&lt;/strong&gt; (default limit is 100, configurable via CLI). All Flex Consumption apps in a subscription and region share a &lt;strong&gt;regional quota of 250 cores&lt;/strong&gt; by default. The formula: instances x cores per instance (0.25 for 512 MB, 1 for 2,048 MB, 2 for 4,096 MB). One app running 1,000 instances at 512 MB consumes the entire quota (1,000 x 0.25 = 250 cores). You can request an increase through Azure support, but plan for this limit when running multiple Flex apps in the same region.&lt;/p&gt;

&lt;h3&gt;
  
  
  The constraints to know about
&lt;/h3&gt;

&lt;p&gt;Flex Consumption comes with real limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One app per plan.&lt;/strong&gt; Consumption and Premium let you put up to 100 function apps on one plan. Flex is one-to-one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No deployment slots.&lt;/strong&gt; &lt;a href="https://learn.microsoft.com/azure/azure-functions/flex-consumption-site-updates" rel="noopener noreferrer"&gt;Rolling updates&lt;/a&gt; are in public preview as an alternative (zero-downtime deployments without slot swaps), but if your deployment strategy depends on slot swaps, this is a blocker today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux only.&lt;/strong&gt; No Windows support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolated worker only.&lt;/strong&gt; The C# in-process model is not supported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App init timeout: 30 seconds.&lt;/strong&gt; If your startup code takes longer, the instance fails to initialize. This is not configurable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blob trigger uses Event Grid only.&lt;/strong&gt; The polling-based blob trigger is not available.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flex Consumption also supports &lt;strong&gt;Azure Files storage mounts&lt;/strong&gt;, letting you mount SMB shares as local directories. This is useful for large binaries, ML models, or shared reference data that you don't want to package in your deployment.&lt;/p&gt;

&lt;p&gt;The Linux-only constraint is less of an issue than it sounds. Linux is where .NET Functions performance is best, and the in-process model (the main reason teams stayed on Windows) is being retired anyway.&lt;/p&gt;

&lt;p&gt;VNet integration works the same way as Premium: subnet delegation to &lt;code&gt;Microsoft.App/environments&lt;/code&gt;, support for private endpoints on storage accounts, Key Vault references over VNet, and native virtual network triggers for non-HTTP event sources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Premium: warm instances, guaranteed
&lt;/h2&gt;

&lt;p&gt;Premium (Elastic Premium) is the plan teams reach for when cold starts become unacceptable. It keeps at least one instance running at all times, so your functions never start from zero. That guarantee comes with a price floor: even with zero traffic, you're billed for that minimum instance.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you get for $146/month
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpynjozl03ym87mgxrcm7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpynjozl03ym87mgxrcm7.png" alt="Premium plan SKUs and minimum monthly cost" width="540" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Billing is per-second based on vCPU-seconds and GB-seconds allocated across instances. No per-execution charge. The EP1 cost breaks down to ~$116.80/vCPU/month + ~$8.32/GB/month at pay-as-you-go rates in US regions. Savings plans (1-year or 3-year commitments) offer roughly 17% off.&lt;/p&gt;

&lt;p&gt;There is no free grant on Premium. From the moment your plan exists, the meter is running.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre-warmed instances and elastic scale
&lt;/h3&gt;

&lt;p&gt;Premium uses two layers to eliminate cold starts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always-ready instances&lt;/strong&gt; run continuously, regardless of load. You configure how many per app, up to 20. These are billed 24/7, executing or not. If you have multiple function apps on the same Premium plan, the plan's minimum instance count equals the highest always-ready count among all apps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prewarmed buffer instances&lt;/strong&gt; sit behind the always-ready pool. The default is 1. When all active instances are handling traffic, the prewarmed instance swaps to active and the platform immediately provisions a new buffer instance to take its place. This means scale-out events get a warm instance instead of a cold one.&lt;/p&gt;

&lt;p&gt;You can define a &lt;strong&gt;warmup trigger&lt;/strong&gt; that runs during the prewarming window. This is where you force-initialize lazy dependencies, open database connections, and prime HTTP connection pools before the instance receives real traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Warmup&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;_httpClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Lazy&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ExpensiveAnalyticsClient&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_analytics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Warmup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Lazy&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ExpensiveAnalyticsClient&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_httpClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_analytics&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Warmup"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;WarmupTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;warmupContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HttpCompletionOption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseHeadersRead&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The warmup trigger only fires during scale-out, not on restarts or deployments. It's available on Premium and Flex Consumption, not on the Consumption plan.&lt;/p&gt;

&lt;p&gt;Elastic scale can burst up to &lt;strong&gt;100 instances on Windows&lt;/strong&gt; and &lt;strong&gt;20-100 on Linux&lt;/strong&gt; depending on region. Scaling beyond the minimum is best-effort: the platform allocates instances as fast as it can, but rapid spikes can outpace the prewarmed buffer. When that happens, you get cold starts even on Premium.&lt;/p&gt;

&lt;h3&gt;
  
  
  VNet and other features
&lt;/h3&gt;

&lt;p&gt;VNet integration is supported but not automatic. You configure it at creation time or after, using regional VNet integration with a dedicated subnet (at least 100 available IPs). Private endpoints for inbound traffic are fully supported: you can create a private IP in your VNet and restrict all public access.&lt;/p&gt;

&lt;p&gt;Non-HTTP triggers from VNet-secured resources (Service Bus with private endpoints, for example) require enabling &lt;strong&gt;Runtime Scale Monitoring&lt;/strong&gt;. Without it, the scale controller can't read the event source metrics to decide when to scale.&lt;/p&gt;

&lt;p&gt;Other features that set Premium apart:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Execution timeout&lt;/strong&gt;: 30 minutes default, configurable to &lt;strong&gt;unbounded&lt;/strong&gt;. Consumption caps at 10 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment slots&lt;/strong&gt;: 3 (including production). Consumption gets 2, Flex gets 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apps per plan&lt;/strong&gt;: up to 100 function apps on a single Premium plan, sharing the VM pool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Linux container images&lt;/strong&gt; are supported.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When Premium is the wrong call
&lt;/h3&gt;

&lt;p&gt;The most common mistake is jumping to Premium from Consumption solely because of cold starts, without evaluating the alternatives.&lt;/p&gt;

&lt;p&gt;If VNet was your only reason, Flex Consumption now gives you VNet integration with scale-to-zero pricing. No need to pay $146/month for network access.&lt;/p&gt;

&lt;p&gt;If your workload is sporadic (a few hundred invocations a day), the math doesn't work. That function costs pennies on Consumption. On Premium EP1, it costs $146/month regardless of usage. The cold start tax has to be genuinely painful to justify that gap.&lt;/p&gt;

&lt;p&gt;And watch the SKU names. EP1 is Elastic Premium. P1v2 is a Dedicated App Service plan. They behave completely differently: EP1 scales dynamically based on event volume, P1v2 gives you a fixed VM that you scale manually. If your Terraform or Bicep has &lt;code&gt;sku = "P1v2"&lt;/code&gt; and you expected autoscaling, check again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dedicated: fixed compute, fixed bill
&lt;/h2&gt;

&lt;p&gt;The Dedicated plan runs your functions on a standard App Service plan. Same infrastructure, same pricing, same scaling model as a web app. Multiple function apps and web apps can share the same plan.&lt;/p&gt;

&lt;p&gt;This is the plan you pick when you already have App Service infrastructure and want to add functions without creating a separate billing line item.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pricing and compute
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fii7tr5hhn2v83z1jgydt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fii7tr5hhn2v83z1jgydt.png" alt="Dedicated plan SKUs and approximate monthly cost" width="484" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are Windows pay-as-you-go prices for US East. Linux is cheaper (roughly 40-50% less for P-series tiers). P1v2 is a previous-generation SKU; Microsoft recommends P1v3 for new deployments.&lt;/p&gt;

&lt;p&gt;Billing is hourly, prorated to the second, per scaled-out instance. Reserved instances (1-year or 3-year) can save up to 55% on Linux. The cost is fixed: you pay the same whether your functions execute zero times or a million times per day.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scaling: you manage it
&lt;/h3&gt;

&lt;p&gt;There is no event-driven scaling on Dedicated. The scale controller that powers Consumption and Premium does not apply here.&lt;/p&gt;

&lt;p&gt;Your options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manual scale-out&lt;/strong&gt;: set the instance count in the portal or via CLI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule-based autoscale&lt;/strong&gt; (Standard tier and above): trigger scale-out based on CPU percentage, memory usage, or a schedule&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Autoscale on App Service is slower than Premium's elastic scale. It reacts to sustained load patterns, not individual event bursts. App Service also has a newer "automatic scaling" feature for HTTP-based traffic, but it's &lt;strong&gt;not supported&lt;/strong&gt; when Functions apps are in the plan.&lt;/p&gt;

&lt;p&gt;Maximum instances: 10-30 per plan, or 100 in an App Service Environment (ASE).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always On must be enabled&lt;/strong&gt; in the App Service configuration. Without it, the Functions runtime goes idle after a period of inactivity. Unlike Consumption's scale-to-zero (which the platform manages), an idle Dedicated plan just means your functions silently stop processing. You're still billed for the compute.&lt;/p&gt;

&lt;h3&gt;
  
  
  When Dedicated fits
&lt;/h3&gt;

&lt;p&gt;Dedicated makes sense in specific circumstances:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You already have an underutilized App Service plan.&lt;/strong&gt; Adding functions to existing compute costs nothing extra. The plan is already paid for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You run mixed workloads.&lt;/strong&gt; A web app and a set of background processing functions on the same plan, sharing resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need deployment slots.&lt;/strong&gt; Up to 20, far more than Premium's 3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predictable billing matters more than efficiency.&lt;/strong&gt; Some finance teams prefer a fixed monthly line item over variable serverless costs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The downside is resource contention. If your web app and function app share an S1 instance and the web app spikes, your function throughput drops. There's no isolation within the plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cold start mitigation: what to try first
&lt;/h2&gt;

&lt;p&gt;If you're staying on Consumption or Flex Consumption, cold starts are part of the deal. The strategies below are ordered by impact, highest first. Not all of them apply to every plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. ReadyToRun compilation
&lt;/h3&gt;

&lt;p&gt;The single highest-impact change for .NET cold starts on Consumption and Flex Consumption. Two lines in your &lt;code&gt;.csproj&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PublishReadyToRun&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/PublishReadyToRun&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;RuntimeIdentifier&amp;gt;&lt;/span&gt;linux-x64&lt;span class="nt"&gt;&amp;lt;/RuntimeIdentifier&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ReadyToRun pre-compiles your assemblies to native code. The JIT compiler still runs for hot paths at runtime, but the initial load skips the bulk of compilation overhead. In practice, this cuts cold start time roughly in half.&lt;/p&gt;

&lt;p&gt;The trade-off: your deployment package grows 2-3x because the assemblies contain both the native precompiled code and the original IL. For a typical Functions app, that's still well under the 1 GB deployment limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Placeholder optimization for .NET isolated
&lt;/h3&gt;

&lt;p&gt;The Functions platform can pre-provision a worker process before your app code loads. Enable it with an app setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WEBSITE_USE_PLACEHOLDER_DOTNETISOLATED=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This requires .NET 6+, a 64-bit process, and the latest Azure Functions SDK versions. The placeholder worker starts the .NET runtime and gets the IPC channel ready while your code is still being loaded, shaving off part of the startup sequence.&lt;/p&gt;

&lt;p&gt;Combine this with ReadyToRun for the best result on Consumption.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Trim your DI registrations
&lt;/h3&gt;

&lt;p&gt;Every service you register in &lt;code&gt;Program.cs&lt;/code&gt; adds to startup time. On a warm instance this is negligible. On a cold start, it compounds.&lt;/p&gt;

&lt;p&gt;Register HTTP clients and SDK clients as &lt;strong&gt;singletons&lt;/strong&gt; so they're constructed once and reused. Wrap expensive dependencies in &lt;code&gt;Lazy&amp;lt;T&amp;gt;&lt;/code&gt; so they're only built when a function actually needs them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SocketsHttpHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;PooledConnectionLifetime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;BaseAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.example.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Lazy&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ExpensiveAnalyticsClient&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Lazy&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ExpensiveAnalyticsClient&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ExpensiveAnalyticsClient&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ExpensiveAnalyticsClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;PooledConnectionLifetime&lt;/code&gt; on &lt;code&gt;SocketsHttpHandler&lt;/code&gt; rotates DNS entries without disposing the &lt;code&gt;HttpClient&lt;/code&gt; instance. This avoids socket exhaustion (the same problem &lt;code&gt;IHttpClientFactory&lt;/code&gt; solves, but without requiring per-request factory calls in a singleton context).&lt;/p&gt;

&lt;p&gt;Fewer functions per app also helps. Each function adds discovery and registration overhead at startup.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Warmup trigger (Premium and Flex Consumption only)
&lt;/h3&gt;

&lt;p&gt;On plans that support prewarmed instances, the warmup trigger lets you run initialization code before the instance takes real traffic. Force-construct your lazy dependencies, open database connections, and send a throwaway HTTP request to prime the connection pool. See the Premium section above for the code.&lt;/p&gt;

&lt;p&gt;The warmup trigger only fires during scale-out. It does not fire on restarts, deployments, or slot swaps. One per app, and the function must be named &lt;code&gt;Warmup&lt;/code&gt; (case-insensitive).&lt;/p&gt;

&lt;h3&gt;
  
  
  What works where
&lt;/h3&gt;

&lt;p&gt;Not every strategy applies to every plan:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh7wszpvs3yhiekqacu6u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh7wszpvs3yhiekqacu6u.png" alt="Cold start mitigation strategies by plan" width="800" height="295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On Dedicated with Always On enabled, cold start is largely a non-issue because instances stay running. On Premium, the always-ready and prewarmed instances handle most of it. ReadyToRun and DI trimming matter most on the serverless plans where instances start from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a plan: the decision matrix
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F73h99g2hnnl4smb74ovd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F73h99g2hnnl4smb74ovd.png" alt="Decision matrix: hosting plan comparison" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Which plan for which workload
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Consumption&lt;/strong&gt; if your traffic is sporadic, you don't need VNet access, and your users can tolerate a few seconds of cold start. Timer triggers, low-volume queue processors, webhook receivers that aren't latency-sensitive. If your bill on Consumption is under $10/month, there's no reason to move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flex Consumption&lt;/strong&gt; if you need VNet integration or more than 200 instances, but still want scale-to-zero pricing. Evaluate this before jumping to Premium. The always-ready instances give you a dial between pure serverless and always-warm, and you pay only for what you configure. The constraints (one app per plan, no deployment slots, Linux only) are the deciding factors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Premium EP1&lt;/strong&gt; if your HTTP endpoints are latency-sensitive and cold starts are genuinely costing you users or revenue. Also the right choice for functions that run continuously or need more than 10 minutes of execution time. If you're running multiple function apps, a shared Premium plan can amortize the $146/month minimum across them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dedicated&lt;/strong&gt; if you already have an App Service plan with spare capacity, need more than 3 deployment slots, or your finance team requires a fixed monthly line item. Don't create a Dedicated plan specifically for Functions unless you have a concrete reason: the lack of event-driven scaling makes it the least "serverless" option.&lt;/p&gt;

&lt;h3&gt;
  
  
  The mistake to avoid
&lt;/h3&gt;

&lt;p&gt;The most common path is: start on Consumption, hit cold start problems in production, jump straight to Premium at $146/month. Flex Consumption sits between them and didn't exist when many teams made that decision. If you're evaluating today, Flex Consumption with 1-2 always-ready instances gives you warm starts with scale-to-zero pricing for on-demand instances. Test it before committing to Premium's minimum.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Are you running Consumption or Premium in production right now?&lt;/strong&gt;&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions Beyond the Basics&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Continues from &lt;a href="https://dev.to/martin_oehlert/series/32874"&gt;Azure Functions for .NET Developers&lt;/a&gt; (Parts 1-9)&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/running-azure-functions-in-docker-why-and-how-1hal"&gt;Running Azure Functions in Docker: Why and How&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/docker-pitfalls-i-hit-and-how-to-avoid-them-2395"&gt;Docker Pitfalls I Hit (And How to Avoid Them)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 3: Scaling Azure Functions: Consumption vs Premium vs Dedicated (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>serverless</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Docker Pitfalls I Hit (And How to Avoid Them)</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 24 Apr 2026 05:39:32 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/docker-pitfalls-i-hit-and-how-to-avoid-them-2395</link>
      <guid>https://dev.to/martin_oehlert/docker-pitfalls-i-hit-and-how-to-avoid-them-2395</guid>
      <description>&lt;p&gt;Your Dockerfile builds, your container starts, and your triggers never fire. The Functions host logs "no functions found" or the container sits idle, processing nothing. The gap between a working image and a working function app is entirely configuration. The runtime needs specific environment variables, the build must publish to the exact path the host expects, and Azurite connections behave differently inside a container network than on localhost. Four walls, four fixes. All code samples are in the &lt;a href="https://github.com/martinoehlert/azure-functions-samples" rel="noopener noreferrer"&gt;companion repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 1: Environment Variables That Vanish
&lt;/h2&gt;

&lt;p&gt;Your container starts, the Functions host initializes, and the logs show this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[2026-04-20T08:12:03Z] No job functions found. Try making your job classes and methods public.
[2026-04-20T08:12:03Z] If you're using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.)
[2026-04-20T08:12:03Z] make sure you've called the registration method for the extension(s)
[2026-04-20T08:12:03Z] in your startup code
[2026-04-20T08:12:03Z] 0 functions loaded
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You check your code. The classes are public. The methods are public. The bindings are registered. Everything runs fine with &lt;code&gt;func start&lt;/code&gt; on your machine.&lt;/p&gt;

&lt;p&gt;The error is misleading. The real cause: &lt;strong&gt;&lt;code&gt;FUNCTIONS_WORKER_RUNTIME&lt;/code&gt;&lt;/strong&gt; is not set.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the file you trusted does not exist here
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;local.settings.json&lt;/code&gt; is a dev-time convenience. The Azure Functions Core Tools reads it when you run &lt;code&gt;func start&lt;/code&gt; locally. Inside a container, that file is never loaded. The container runtime reads OS environment variables only, and if &lt;code&gt;FUNCTIONS_WORKER_RUNTIME&lt;/code&gt; is missing, the host cannot determine which language worker to start. It discovers zero functions and prints an error that sends you looking at your code instead of your configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;AzureWebJobsStorage&lt;/code&gt;&lt;/strong&gt; is the second variable that catches people. Without it, you get a different failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Value cannot be null. (Parameter 'connectionString')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or worse, no error at all. HTTP triggers still work because they do not require storage. You test with an HTTP endpoint, everything responds, you deploy, and your queue triggers silently never fire. The host needs a storage connection to manage leases, checkpoints, and timer schedules for every non-HTTP trigger type.&lt;/p&gt;

&lt;p&gt;If you set &lt;code&gt;FUNCTIONS_WORKER_RUNTIME&lt;/code&gt; to &lt;code&gt;dotnet&lt;/code&gt; instead of &lt;code&gt;dotnet-isolated&lt;/code&gt;, the host raises &lt;strong&gt;AZFD0013&lt;/strong&gt;: the configured runtime does not match the worker runtime metadata in your published artifacts. Another error that points away from the actual one-word fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  The second trap: &lt;code&gt;.env&lt;/code&gt; files that silently mangle values
&lt;/h3&gt;

&lt;p&gt;Azure Storage connection strings are long. If your &lt;code&gt;.env&lt;/code&gt; file wraps them across lines, Docker Compose silently truncates or corrupts the value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Broken: line-wrapped connection string
AzureWebJobsStorage=DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;
  AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;
  BlobEndpoint=http://azurite:10000/devstoreaccount1;
  QueueEndpoint=http://azurite:10001/devstoreaccount1;

# Working: entire value on one line
AzureWebJobsStorage=DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;QueueEndpoint=http://azurite:10001/devstoreaccount1;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No warning, no parse error. The value just stops at the first newline.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: separate constants from secrets
&lt;/h3&gt;

&lt;p&gt;Bake values that never change per environment into your Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; AzureWebJobsScriptRoot=/home/site/wwwroot&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; FUNCTIONS_WORKER_RUNTIME=dotnet-isolated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass everything else through your Compose file or deployment config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;functions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;AzureWebJobsStorage=${AzureWebJobsStorage}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;APPLICATIONINSIGHTS_CONNECTION_STRING=${APPLICATIONINSIGHTS_CONNECTION_STRING}&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connection strings and instrumentation keys stay out of the image. They come from &lt;code&gt;.env&lt;/code&gt; locally and from app settings or Key Vault references in production.&lt;/p&gt;

&lt;p&gt;One more if you are deploying custom containers to &lt;strong&gt;App Service&lt;/strong&gt; specifically: set &lt;code&gt;WEBSITES_ENABLE_APP_SERVICE_STORAGE=false&lt;/code&gt;. The default (&lt;code&gt;true&lt;/code&gt;) mounts persistent storage over &lt;code&gt;/home&lt;/code&gt;, which overwrites your published function code at startup. This does not apply to Container Apps, only App Service (&lt;a href="https://github.com/Azure/azure-functions-docker/issues/642" rel="noopener noreferrer"&gt;GitHub issue #642&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 2: Azurite and Docker Networking
&lt;/h2&gt;

&lt;p&gt;Most tutorials tell you to set &lt;code&gt;AzureWebJobsStorage&lt;/code&gt; to &lt;code&gt;UseDevelopmentStorage=true&lt;/code&gt; and move on. That shorthand expands to a full connection string pointing at localhost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DefaultEndpointsProtocol&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http;&lt;/span&gt;
&lt;span class="py"&gt;AccountName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;devstoreaccount1;&lt;/span&gt;
&lt;span class="py"&gt;AccountKey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;&lt;/span&gt;
&lt;span class="py"&gt;BlobEndpoint&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:10000/devstoreaccount1;&lt;/span&gt;
&lt;span class="py"&gt;QueueEndpoint&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:10001/devstoreaccount1;&lt;/span&gt;
&lt;span class="py"&gt;TableEndpoint&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:10002/devstoreaccount1;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See those &lt;code&gt;127.0.0.1&lt;/code&gt; addresses? When Azurite runs on your machine, that works fine. Inside Docker, it breaks.&lt;/p&gt;

&lt;p&gt;Each container runs in its own &lt;strong&gt;network namespace&lt;/strong&gt;. &lt;code&gt;127.0.0.1&lt;/code&gt; inside the functions container refers to the functions container itself, not Azurite. Your function tries to reach storage on its own loopback interface, finds nothing listening, and fails silently or throws a connection error depending on the trigger type.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg9itv2gjh5jrqrfygjnf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg9itv2gjh5jrqrfygjnf.png" alt="Docker networking: broken UseDevelopmentStorage=true vs working explicit service name" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docker Compose creates a &lt;strong&gt;shared bridge network&lt;/strong&gt; where each service name resolves to the corresponding container's IP. So the fix is to spell out the full connection string with the Compose service name replacing &lt;code&gt;127.0.0.1&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DefaultEndpointsProtocol&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http;&lt;/span&gt;
&lt;span class="py"&gt;AccountName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;devstoreaccount1;&lt;/span&gt;
&lt;span class="py"&gt;AccountKey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;&lt;/span&gt;
&lt;span class="py"&gt;BlobEndpoint&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http://azurite:10000/devstoreaccount1;&lt;/span&gt;
&lt;span class="py"&gt;QueueEndpoint&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http://azurite:10001/devstoreaccount1;&lt;/span&gt;
&lt;span class="py"&gt;TableEndpoint&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http://azurite:10002/devstoreaccount1;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;azurite&lt;/code&gt; here is whatever you named the service in your &lt;code&gt;docker-compose.yml&lt;/code&gt;. DNS resolution happens automatically on the Compose network.&lt;/p&gt;

&lt;p&gt;But DNS resolving correctly is not enough. By default, Azurite binds to &lt;code&gt;127.0.0.1&lt;/code&gt; inside its own container, which means it only accepts connections from itself. You need to pass &lt;strong&gt;&lt;code&gt;--blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0&lt;/code&gt;&lt;/strong&gt; so Azurite listens on all interfaces. Without this, the functions container resolves &lt;code&gt;azurite&lt;/code&gt; to the right IP, opens a TCP connection, and gets "Connection refused."&lt;/p&gt;

&lt;p&gt;This pitfall hides well because &lt;strong&gt;HTTP triggers don't need storage&lt;/strong&gt;. You build a function app, add an HTTP trigger, test it in Docker, everything works. Then you add a queue trigger and it silently does nothing: no errors in the console, no messages processed, no indication that storage is unreachable. The function host quietly skips triggers it can't initialize.&lt;/p&gt;

&lt;p&gt;A quick connectivity check saves you the debugging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;functions curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://azurite:10000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Azurite is reachable and bound correctly, you get back a short XML or text response. If you get "Connection refused," check the bind flags. If you get a DNS error, check your service name.&lt;/p&gt;

&lt;p&gt;Part 1 already showed the working Compose file with these settings in place. That is why each piece is there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 3: Debugging a Silent Container
&lt;/h2&gt;

&lt;p&gt;Your container starts, the health check passes, but nothing happens. No HTTP responses, no queue processing, no timer triggers. The logs show the host booting and then silence. This is the most common failure mode with Azure Functions in Docker, and it has six distinct causes. Work through them in order.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc70qsa1xummpjxrl63p5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc70qsa1xummpjxrl63p5.png" alt="Debug decision tree: 6-step diagnostic flow for silent Azure Functions containers" width="800" height="1315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Check if the host found your functions.&lt;/strong&gt; Run &lt;code&gt;docker logs &amp;lt;container&amp;gt;&lt;/code&gt; and look for the function discovery block near startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host initialized (348ms)
Found the following functions:
  ProcessOrder: timerTrigger
  SubmitOrder: httpTrigger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see "Host initialized" but zero functions listed, your &lt;strong&gt;&lt;code&gt;AzureWebJobsScriptRoot&lt;/code&gt;&lt;/strong&gt; is wrong or your Dockerfile's &lt;code&gt;WORKDIR&lt;/code&gt; does not point to &lt;code&gt;/home/site/wwwroot&lt;/code&gt;. The host scans that directory for compiled function metadata. If it points somewhere else, it finds nothing and starts successfully with nothing to run. This is the root cause in &lt;a href="https://github.com/Azure/azure-functions-docker/issues/642" rel="noopener noreferrer"&gt;#642&lt;/a&gt; and &lt;a href="https://github.com/Azure/azure-functions-docker/issues/980" rel="noopener noreferrer"&gt;#980&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Check storage connectivity.&lt;/strong&gt; If your functions are listed but triggers never fire, the problem is almost always storage. Look for this error in the logs:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The Azure Storage connection string named 'Storage' does not exist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Timer triggers and queue triggers need a valid &lt;code&gt;AzureWebJobsStorage&lt;/code&gt; connection to coordinate leases and checkpoints. HTTP triggers work without storage, so a container that responds to HTTP but ignores everything else is a storage configuration problem. Verify your environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;functions &lt;span class="nb"&gt;env&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;FUNCTIONS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This surfaces &lt;code&gt;FUNCTIONS_WORKER_RUNTIME&lt;/code&gt;, &lt;code&gt;AzureWebJobsStorage&lt;/code&gt;, and any other Functions-specific configuration in the running container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Inspect the container filesystem.&lt;/strong&gt; When functions still do not appear after fixing the script root, the published output may not be where you think it is. Check directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;functions &lt;span class="nb"&gt;ls&lt;/span&gt; /home/site/wwwroot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see your &lt;code&gt;.dll&lt;/code&gt; files, &lt;code&gt;host.json&lt;/code&gt;, &lt;code&gt;function.json&lt;/code&gt; files, and the &lt;code&gt;worker.config.json&lt;/code&gt;. A wrong &lt;code&gt;COPY --from=build&lt;/code&gt; path in a multi-stage Dockerfile is the most common cause: the build stage publishes to &lt;code&gt;/app/publish&lt;/code&gt; but the copy targets &lt;code&gt;/app/out&lt;/code&gt;, and the container starts with an empty wwwroot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Check for assembly conflicts.&lt;/strong&gt; If the host discovers your functions but the worker crashes on invocation, look for &lt;code&gt;FileNotFoundException&lt;/code&gt; referencing assemblies like &lt;code&gt;System.Memory.Data&lt;/code&gt;. This happens when &lt;strong&gt;in-process WebJobs SDK packages&lt;/strong&gt; ship inside an isolated worker image. The host and worker expect different assembly versions, and the loader fails silently until a trigger actually fires. Pin your NuGet package versions to match the host's expectations. See &lt;a href="https://github.com/Azure/azure-functions-docker/issues/1221" rel="noopener noreferrer"&gt;#1221&lt;/a&gt; for the specific version matrix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Attach a debugger.&lt;/strong&gt; When the logs tell you nothing useful, attach directly. VS Code's &lt;code&gt;pipeTransport&lt;/code&gt; configuration or Rider's Docker attach both work. The critical detail: the Functions host and the &lt;strong&gt;isolated worker are separate .NET processes&lt;/strong&gt;. The host is the parent process managing triggers; your code runs in the worker. Attach to the worker PID, not the host PID. If you attach to the host, you will see trigger infrastructure but none of your breakpoints will hit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Watch for broken image tags.&lt;/strong&gt; Sometimes your container worked yesterday and fails today with no code changes. Base image tag updates can silently break functions. Tag &lt;code&gt;4.33.2&lt;/code&gt; broke function discovery for days before anyone traced it back to the image itself (&lt;a href="https://github.com/Azure/azure-functions-docker/issues/1068" rel="noopener noreferrer"&gt;#1068&lt;/a&gt;). Always pin specific version tags in your Dockerfile. Never use &lt;code&gt;:latest&lt;/code&gt; for the Functions base image in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Known Issues
&lt;/h3&gt;

&lt;p&gt;A few problems fall outside the decision tree but will bite you eventually:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No graceful shutdown.&lt;/strong&gt; The default entrypoint &lt;code&gt;start.sh&lt;/code&gt; runs as PID 1 and does not forward SIGTERM to child processes. Your container gets SIGKILL after the orchestrator's grace period expires, which means in-flight executions are terminated without cleanup. This has been open for five years (&lt;a href="https://github.com/Azure/azure-functions-docker/issues/404" rel="noopener noreferrer"&gt;#404&lt;/a&gt;). Workaround: use &lt;code&gt;dumb-init&lt;/code&gt; or a custom entrypoint that traps signals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-root containers break startup.&lt;/strong&gt; The Functions host needs write access to &lt;code&gt;/azure-functions-host&lt;/code&gt; at startup. Running the container as a non-root user fails unless you fix directory permissions in your Dockerfile (&lt;a href="https://github.com/Azure/azure-functions-docker/issues/424" rel="noopener noreferrer"&gt;#424&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Development environment restart loops.&lt;/strong&gt; Setting &lt;code&gt;AZURE_FUNCTIONS_ENVIRONMENT=Development&lt;/code&gt; can trigger the host to restart repeatedly as it watches for file changes that never settle (&lt;a href="https://github.com/Azure/azure-functions-docker/issues/1207" rel="noopener noreferrer"&gt;#1207&lt;/a&gt;). Use &lt;code&gt;Production&lt;/code&gt; or &lt;code&gt;Staging&lt;/code&gt; in Docker unless you specifically need development-mode diagnostics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 4: Image Size and Cold Start
&lt;/h2&gt;

&lt;p&gt;The default Azure Functions base image is 800-900 MB. Add your application code, NuGet packages, and assets, and you're over 1 GB before your first request arrives (&lt;a href="https://github.com/Azure/azure-functions-docker/issues/236" rel="noopener noreferrer"&gt;#236&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-slim&lt;/code&gt; tags can paradoxically be &lt;em&gt;larger&lt;/em&gt; than the regular tags (&lt;a href="https://github.com/Azure/azure-functions-docker/issues/1230" rel="noopener noreferrer"&gt;#1230&lt;/a&gt;). Always verify with &lt;code&gt;docker images&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Old extension bundles (v2 and v3) still ship inside the v4 images, wasting roughly 429 MB on code your app will never execute (&lt;a href="https://github.com/Azure/azure-functions-docker/issues/880" rel="noopener noreferrer"&gt;#880&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Every optimization here is measurable. Start with &lt;code&gt;docker images&lt;/code&gt; and track the delta.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fheu8msnklq4aaoooc1kt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fheu8msnklq4aaoooc1kt.png" alt="Docker image layers: before (~1 GB+) vs after (~300-400 MB) optimization" width="800" height="332"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  .dockerignore
&lt;/h3&gt;

&lt;p&gt;Without a &lt;code&gt;.dockerignore&lt;/code&gt;, &lt;code&gt;COPY . .&lt;/code&gt; sends your entire working directory to the Docker daemon, including &lt;code&gt;.git/&lt;/code&gt; history and &lt;code&gt;local.settings.json&lt;/code&gt; (which contains connection strings and keys).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bin/
obj/
.git/
.vs/
.vscode/
local.settings.json
node_modules/
*.user
Dockerfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This alone can cut your build context by hundreds of megabytes and prevent secrets from leaking into image layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer ordering for cache hits
&lt;/h3&gt;

&lt;p&gt;The order of your &lt;code&gt;COPY&lt;/code&gt; instructions determines whether Docker can reuse cached layers. Copy the project file first, restore, then copy everything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; MyFunctionApp.csproj .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;dotnet restore

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;dotnet publish &lt;span class="nt"&gt;-c&lt;/span&gt; Release &lt;span class="nt"&gt;-o&lt;/span&gt; /home/site/wwwroot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When only source code changes, the restore layer stays cached:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;Step 3/7 : RUN dotnet restore
 ---&amp;gt; Using cache
 ---&amp;gt; 4a8b2c1d3e5f
Step 4/7 : COPY . .
 ---&amp;gt; 9f1e2d3c4b5a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;strong&gt;"Using cache"&lt;/strong&gt; line saves 30-120 seconds per build depending on your package count. Without this ordering, every code change re-downloads every NuGet package.&lt;/p&gt;

&lt;h3&gt;
  
  
  ReadyToRun compilation
&lt;/h3&gt;

&lt;p&gt;Add the &lt;strong&gt;&lt;code&gt;PublishReadyToRun&lt;/code&gt;&lt;/strong&gt; flag to pre-compile IL to native code, reducing JIT time at startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;dotnet publish &lt;span class="nt"&gt;-c&lt;/span&gt; Release &lt;span class="nt"&gt;-o&lt;/span&gt; /home/site/wwwroot &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;-p&lt;/span&gt;:PublishReadyToRun&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This increases image size slightly but cuts cold start latency by front-loading compilation to build time instead of request time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trimming&lt;/strong&gt; (&lt;code&gt;PublishTrimmed=true&lt;/code&gt;) is the more aggressive option. It strips unused assemblies and can dramatically reduce image size. But the Functions runtime uses reflection to discover your function endpoints, and the trimmer can remove types it considers unreachable. If your functions disappear after trimming, that's why. Use trimming only if you're willing to maintain trim annotations and test thoroughly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cold start: the numbers that matter
&lt;/h3&gt;

&lt;p&gt;On &lt;strong&gt;Azure Container Apps&lt;/strong&gt;, image pull time dominates cold start because the platform scales to zero:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image size&lt;/th&gt;
&lt;th&gt;Pull time&lt;/th&gt;
&lt;th&gt;Total cold start&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;~480 MB&lt;/td&gt;
&lt;td&gt;~20s&lt;/td&gt;
&lt;td&gt;~25-30s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~140 MB&lt;/td&gt;
&lt;td&gt;~7s&lt;/td&gt;
&lt;td&gt;~12-15s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That 13-second pull difference hits every scale-from-zero event. On &lt;strong&gt;Functions Premium&lt;/strong&gt; with always-ready instances, the image is cached on warm infrastructure, so size matters less for latency. It still matters for deployment speed and registry costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE accumulation
&lt;/h3&gt;

&lt;p&gt;Base images are only rebuilt monthly, so vulnerabilities accumulate between rebuilds (&lt;a href="https://github.com/Azure/azure-functions-docker/issues/1185" rel="noopener noreferrer"&gt;#1185&lt;/a&gt;). A multi-stage build where you copy your published output onto a fresh OS base gives you control over patching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;mcr.microsoft.com/dotnet/sdk:8.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="c"&gt;# ... build steps ...&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /home/site/wwwroot /home/site/wwwroot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;docker images&lt;/code&gt; after applying these changes. A starting point of 1 GB+ dropping to 300-400 MB is typical when you combine layer optimization, proper &lt;code&gt;.dockerignore&lt;/code&gt;, and ReadyToRun instead of carrying dead extension bundles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-Deploy Checklist
&lt;/h2&gt;

&lt;p&gt;Save yourself a repeat debugging session. Run through this before every container deployment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;FUNCTIONS_WORKER_RUNTIME&lt;/code&gt; set to &lt;code&gt;dotnet-isolated&lt;/code&gt; in your container environment, not inherited from &lt;code&gt;local.settings.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;AzureWebJobsStorage&lt;/code&gt; uses explicit endpoint strings with Docker service names instead of &lt;code&gt;UseDevelopmentStorage=true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Connection strings are single-line in &lt;code&gt;.env&lt;/code&gt; files with no line-wrapping&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;docker logs&lt;/code&gt; confirms all expected functions discovered at startup&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;AzureWebJobsScriptRoot&lt;/code&gt; points to &lt;code&gt;/home/site/wwwroot&lt;/code&gt; (verify if using a custom base image)&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;.dockerignore&lt;/code&gt; excludes &lt;code&gt;bin/&lt;/code&gt;, &lt;code&gt;obj/&lt;/code&gt;, &lt;code&gt;.git/&lt;/code&gt;, and &lt;code&gt;local.settings.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] NuGet restore layer cached separately from the source code copy step&lt;/li&gt;
&lt;li&gt;[ ] Base image tag pinned to a specific version, not &lt;code&gt;:latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Azurite bound to &lt;code&gt;0.0.0.0&lt;/code&gt; in your Compose configuration&lt;/li&gt;
&lt;li&gt;[ ] Image tested with &lt;code&gt;docker compose up&lt;/code&gt; locally before pushing to any registry&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Which of these four pitfalls cost you the most debugging time: environment variables, Azurite networking, silent startup failures, or image size?&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions Beyond the Basics&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/running-azure-functions-in-docker-why-and-how-1hal"&gt;Running Azure Functions in Docker: Why and How&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 2: Docker Pitfalls I Hit (And How to Avoid Them) (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>docker</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>From AZ-204 to AI-200: What Changed and Why It Matters</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 17 Apr 2026 21:18:34 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/from-az-204-to-ai-200-what-changed-and-why-it-matters-5glh</link>
      <guid>https://dev.to/martin_oehlert/from-az-204-to-ai-200-what-changed-and-why-it-matters-5glh</guid>
      <description>&lt;p&gt;Comparing the AZ-204 skill outline against the AI-200 course structure, roughly 60% of AZ-204 carries forward, 25% is dropped entirely, and AI-200 adds about 30% net-new content that AZ-204 never touched. Which side of that split you land on determines whether this transition is a week of review or a month of study. The gap is lopsided enough that you cannot assume existing knowledge transfers cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is AI-200?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Full name:&lt;/strong&gt; AI-200: Azure AI Cloud Developer Associate&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Format:&lt;/strong&gt; Multiple choice, case studies, and scenario-based questions. Based on &lt;a href="https://learn.microsoft.com/en-us/credentials/certifications/certification-exams" rel="noopener noreferrer"&gt;standard Microsoft exam format&lt;/a&gt;: approximately 40-60 questions, 100-minute window, passing score around 700/1000.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeline:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Beta exam: April 2026&lt;/li&gt;
&lt;li&gt;General availability: July 2026 (estimated)&lt;/li&gt;
&lt;li&gt;AZ-204 retirement: &lt;a href="https://learn.microsoft.com/en-us/credentials/certifications/exams/az-204/" rel="noopener noreferrer"&gt;July 31, 2026&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Course:&lt;/strong&gt; The AI-200T00 instructor-led training course maps to seven learning paths that define the exam scope:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Container Hosting: ACR, App Service containers, Container Apps, AKS&lt;/li&gt;
&lt;li&gt;Cosmos DB: NoSQL API with vector search and AI integration&lt;/li&gt;
&lt;li&gt;PostgreSQL Vector Search: pgvector, HNSW indexes, hybrid search&lt;/li&gt;
&lt;li&gt;Azure Managed Redis: data operations, event messaging, vector storage&lt;/li&gt;
&lt;li&gt;Backend Services: Service Bus, Event Grid, Azure Functions&lt;/li&gt;
&lt;li&gt;Secrets and Configuration: Key Vault, managed identities, App Configuration&lt;/li&gt;
&lt;li&gt;Observability: OpenTelemetry, Azure Monitor logs and metrics&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Carried Forward, What Got Dropped, What's New
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Carried forward (~60% of AZ-204)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most of the backend services survive: Azure Functions (triggers, bindings, Durable Functions), Service Bus, Event Grid, Key Vault, App Configuration, and managed identities all carry over. Several topics are expanded rather than simply retained. Container Apps now gets deeper coverage of KEDA scaling and Dapr integration. Cosmos DB adds vector search on top of the existing NoSQL API. Container Registry picks up ACR Tasks. And managed identities extend to AKS workload identity, which matters because AKS is one of the largest new additions. If you already hold AZ-204, this 60% is review, not new study.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dropped (~25% of AZ-204)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Comparing the &lt;a href="https://learn.microsoft.com/en-us/credentials/certifications/resources/study-guides/az-204" rel="noopener noreferrer"&gt;AZ-204 study guide&lt;/a&gt; against the AI-200 course outline, seven topics are gone entirely: Blob Storage SDK, MSAL/Identity Platform, Microsoft Graph, SAS tokens, API Management, Event Hubs, and Azure Container Instances. Microsoft removed the CRUD-oriented cloud app topics that do not serve AI workloads. You will not be tested on generating SAS tokens or calling Graph endpoints. If you spent weeks on MSAL token flows for AZ-204, that knowledge still applies to real projects, but it will not appear on AI-200.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brand new (~30% of AI-200)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Based on the &lt;a href="https://learn.microsoft.com/en-us/training/courses/ai-200t00" rel="noopener noreferrer"&gt;AI-200T00 course structure&lt;/a&gt;, AKS spans three modules and likely accounts for an estimated 20-25 exam questions. You need cluster creation with &lt;code&gt;kubectl&lt;/code&gt;, ACR integration via &lt;code&gt;--attach-acr&lt;/code&gt;, and scaling with HPA and cluster autoscaler. Configuration covers ConfigMaps, Secrets, Key Vault CSI Driver, persistent storage (Azure Disk for RWO, Azure Files for RWX), taints and tolerations, and the difference between resource requests and limits. Monitoring adds Container Insights, KQL queries for pod status and events, managed Prometheus, and alerting on OOMKills and resource exhaustion. This is the single largest new topic by question count.&lt;/p&gt;

&lt;p&gt;PostgreSQL with pgvector is where AI-200 tests your understanding of vector databases, covering an estimated 8-11 questions across three modules. The foundation is Flexible Server provisioning with Entra ID auth and PgBouncer connection pooling. From there, you enable the pgvector extension for vector storage and work with distance operators: L2 (&lt;code&gt;&amp;lt;-&amp;gt;&lt;/code&gt;), cosine (&lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt;), and inner product (&lt;code&gt;&amp;lt;#&amp;gt;&lt;/code&gt;). Batch embedding pipelines use Azure OpenAI to generate vectors at scale. Index optimization is where it gets specific: IVFFlat (partition-based, best under 100K vectors with frequent updates) versus HNSW (graph-based, best above 500K static vectors). Hybrid search combines vector similarity with metadata filters using standard &lt;code&gt;WHERE&lt;/code&gt; clauses.&lt;/p&gt;

&lt;p&gt;Azure Managed Redis replaces the narrow "Azure Cache for Redis" coverage from AZ-204 with a broader scope across an estimated 7-12 questions. Five core data types (strings, hashes, lists, sets, sorted sets) and caching patterns (cache-aside, write-through, write-behind) form the baseline. The exam also tests event messaging: Pub/Sub for fire-and-forget broadcasting versus Streams for durable at-least-once delivery with consumer groups (&lt;code&gt;XREADGROUP&lt;/code&gt;, &lt;code&gt;XACK&lt;/code&gt;). On the Enterprise tier, RediSearch enables vector similarity search using FLAT and HNSW indexes combined with tag, numeric, and text filters.&lt;/p&gt;

&lt;p&gt;OpenTelemetry rounds out the new content with an estimated 5-7 questions. The Azure Monitor OpenTelemetry Distro provides a one-line setup via &lt;code&gt;UseAzureMonitor()&lt;/code&gt;, replacing the proprietary Application Insights SDK. Custom spans use &lt;code&gt;ActivitySource&lt;/code&gt;, custom metrics use &lt;code&gt;Meter&lt;/code&gt; instruments, and W3C TraceContext propagation handles distributed trace correlation across services. Sampling strategies control telemetry volume and cost, which is the kind of production concern the exam now prioritizes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Shifts Worth Understanding
&lt;/h2&gt;

&lt;p&gt;The topic changes above are not random. They reflect a different definition of what an Azure developer does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vector databases replace blob storage
&lt;/h3&gt;

&lt;p&gt;If your application retrieves context from a knowledge base before passing it to a language model, you are building a RAG pipeline, and the retrieval layer runs on one of three backends the exam now tests.&lt;/p&gt;

&lt;p&gt;Cosmos DB supports vector search for globally distributed workloads. PostgreSQL with pgvector handles complex hybrid queries where you combine vector similarity with metadata filters in standard &lt;code&gt;WHERE&lt;/code&gt; clauses. Redis provides low-latency vector retrieval on the Enterprise tier using FLAT and HNSW indexes. Each backend has different index types (IVFFlat, HNSW, FLAT), different distance operators, and different tradeoffs around dataset size and query complexity.&lt;/p&gt;

&lt;p&gt;AZ-204 treated storage as a CRUD problem: upload blobs, set access tiers, generate SAS tokens. AI-200 treats storage as a search problem, and the skill gap between "call a PUT endpoint" and "choose the right index type for 500K embeddings" is not small.&lt;/p&gt;

&lt;h3&gt;
  
  
  AKS moves from infrastructure to developer concern
&lt;/h3&gt;

&lt;p&gt;AI workloads need GPU-enabled nodes isolated from general compute, custom operators, and fine-grained resource limits. Container Apps cannot give you any of that. AI-200 assigns three full modules to AKS: deployment, configuration, and monitoring.&lt;/p&gt;

&lt;p&gt;The exam expects you to select between Azure Disk (RWO) and Azure Files (RWX) storage classes, integrate secrets through the Key Vault CSI Driver, and manage node pools with taints and tolerations. Monitoring means writing KQL queries against Container Insights to diagnose pod failures and resource exhaustion. AZ-204 kept you at the Container Apps level, where Kubernetes was an implementation detail you never touched. That abstraction no longer holds when your inference service needs a dedicated A100 node pool with specific resource requests and limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenTelemetry replaces proprietary instrumentation
&lt;/h3&gt;

&lt;p&gt;Your tracing code now works the same whether telemetry flows to Azure Monitor, Jaeger, or Datadog. The Application Insights SDK locked you into Microsoft's instrumentation API, Microsoft's backend, and Microsoft's query tools. AI-200 replaces that instrumentation layer with &lt;a href="https://opentelemetry.io/" rel="noopener noreferrer"&gt;OpenTelemetry&lt;/a&gt;, the CNCF-backed open standard.&lt;/p&gt;

&lt;p&gt;The Azure Monitor OpenTelemetry Distro makes setup a one-liner with &lt;code&gt;UseAzureMonitor()&lt;/code&gt;, but the exam goes deeper. Custom instrumentation means creating spans with &lt;code&gt;ActivitySource&lt;/code&gt; and recording metrics with &lt;code&gt;Meter&lt;/code&gt; instruments. Distributed trace correlation relies on W3C TraceContext headers propagated across service boundaries. Sampling configuration controls telemetry volume, which directly affects cost at scale. Azure Monitor still serves as the analysis backend; what changed is the instrumentation contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means for Your Study Plan
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you already hold AZ-204:&lt;/strong&gt; roughly 60% carries forward. Your knowledge of Azure Functions, Service Bus, Event Grid, Key Vault, managed identities, and Cosmos DB basics is still valid. The gap areas are AKS (the largest single investment if you have not worked with Kubernetes), PostgreSQL with pgvector, Azure Managed Redis vector storage and Streams, and OpenTelemetry custom instrumentation. Budget 3-4 weeks of focused study on those new topics, then 1 week reviewing carried-over material to make sure nothing has shifted in scope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you are currently studying for AZ-204:&lt;/strong&gt; you have a decision to make before &lt;a href="https://learn.microsoft.com/en-us/credentials/certifications/exams/az-204/" rel="noopener noreferrer"&gt;July 31, 2026&lt;/a&gt;. If you are close to passing, finish it: the credential stays valid through its &lt;a href="https://learn.microsoft.com/en-us/credentials/certifications/renew" rel="noopener noreferrer"&gt;full renewal cycle&lt;/a&gt;. If you are early in your studies, pivot to AI-200 now and skip the dropped topics entirely. There is no reason to invest time in the Blob Storage SDK, MSAL, Microsoft Graph, API Management, or Event Hubs when those topics will not appear on the replacement exam.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you are starting fresh:&lt;/strong&gt; go directly to AI-200. The AI-200T00 course structure and Microsoft Learn paths give you everything you need; AZ-204 material adds no value at this point.&lt;/p&gt;

&lt;p&gt;The 8-week plan below assumes you are starting from scratch or pivoting from early AZ-204 study:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fazmbeqgvzb5d3hkiqtei.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fazmbeqgvzb5d3hkiqtei.png" alt="8-week AI-200 study plan" width="538" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://learn.microsoft.com/en-us/training/courses/ai-200t00" rel="noopener noreferrer"&gt;AI-200T00 course&lt;/a&gt; and the Microsoft Learn paths aligned to each domain are the primary resources once they publish alongside the beta exam. Are you finishing AZ-204 before July or pivoting to AI-200 now?&lt;/p&gt;

</description>
      <category>azure</category>
      <category>certification</category>
      <category>career</category>
      <category>learning</category>
    </item>
    <item>
      <title>Running Azure Functions in Docker: Why and How</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 17 Apr 2026 05:23:51 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/running-azure-functions-in-docker-why-and-how-1hal</link>
      <guid>https://dev.to/martin_oehlert/running-azure-functions-in-docker-why-and-how-1hal</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions Beyond the Basics&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Continues from &lt;a href="https://dev.to/martin_oehlert/series/38960"&gt;Azure Functions for .NET Developers&lt;/a&gt; (Parts 1-9)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When zip-deploy stops fitting
&lt;/h2&gt;

&lt;p&gt;Your Azure Function needs to generate PDF invoices, so you add Puppeteer to your project. Zip-deploy works fine on your machine, but the Consumption plan doesn't have the Chromium dependencies installed. The function throws a cryptic error about missing shared libraries, and you're stuck choosing between a workaround that limits your architecture or a deployment model that gives you full control over the OS.&lt;/p&gt;

&lt;p&gt;Most Azure Functions never hit this wall. &lt;strong&gt;Zip-deploy&lt;/strong&gt; and &lt;strong&gt;run-from-package&lt;/strong&gt; handle the majority of workloads well: your code and dependencies get packaged, uploaded, and run on Microsoft's managed infrastructure. You don't think about the OS, the runtime image, or what's installed underneath. That's the point, and it's a good default.&lt;/p&gt;

&lt;p&gt;Containerizing a Function adds real operational cost. You own the base image, the patching cycle, the registry, and the build pipeline. If zip-deploy already works, containerizing your Function adds overhead with no payoff.&lt;/p&gt;

&lt;p&gt;But there are specific problems where Docker earns that overhead back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native dependencies&lt;/strong&gt; are the most common trigger. FFmpeg for media processing, Puppeteer or Playwright for headless browser work, libgdiplus for image manipulation: these require OS-level packages that the default Azure Functions host doesn't include. A custom Docker image lets you install exactly what the function needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reproducible builds across environments&lt;/strong&gt; matter when your team needs the same OS, the same SDK version, and the same native tooling from local dev through staging to production. A Dockerfile pins all of it in version control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running Functions alongside other containers&lt;/strong&gt; is the third case. If you're already deploying to &lt;strong&gt;Azure Container Apps&lt;/strong&gt; or &lt;strong&gt;AKS&lt;/strong&gt;, packaging your Function as a container lets it sit next to your APIs, workers, and sidecars in the same orchestration layer. One deployment model, one scaling configuration, one set of infrastructure to manage.&lt;/p&gt;

&lt;p&gt;If one of those three problems is yours, the container tax is worth paying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dockerfile: multi-stage build for .NET 10
&lt;/h2&gt;

&lt;p&gt;Start with the complete Dockerfile, then walk through what each stage does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;--platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:10.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /src&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; *.csproj .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;dotnet restore

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;dotnet publish &lt;span class="nt"&gt;-c&lt;/span&gt; Release &lt;span class="nt"&gt;-o&lt;/span&gt; /app/publish

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;--platform=linux/amd64 mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated10.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runtime&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /home/site/wwwroot&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/publish .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fourteen lines. That's the whole thing.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--platform=linux/amd64&lt;/code&gt; flag on both &lt;code&gt;FROM&lt;/code&gt; lines pins the image architecture. The Azure Functions base images only ship for &lt;code&gt;linux/amd64&lt;/code&gt;, so without this flag, builds on Apple Silicon pull the wrong manifest and fail. Pinning the platform makes the Dockerfile work identically on Intel and ARM machines.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;build stage&lt;/strong&gt; uses the .NET 10 SDK image to compile your project. The &lt;code&gt;COPY *.csproj&lt;/code&gt; then &lt;code&gt;dotnet restore&lt;/code&gt; pattern caches NuGet packages in a Docker layer, so subsequent builds skip the restore unless your dependencies change. The &lt;code&gt;dotnet publish&lt;/code&gt; step compiles your code and produces a deployment-ready output in &lt;code&gt;/app/publish&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;runtime stage&lt;/strong&gt; switches to the Azure Functions base image. This image ships with the Functions host process, the dotnet-isolated worker runtime, and the three environment variables your app needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AzureWebJobsScriptRoot=/home/site/wwwroot&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FUNCTIONS_WORKER_RUNTIME=dotnet-isolated&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AzureFunctionsJobHost__Logging__Console__IsEnabled=true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need to set any of these yourself. The base image handles it. Your only job is to place the published output at &lt;code&gt;/home/site/wwwroot&lt;/code&gt;, which is why &lt;code&gt;WORKDIR&lt;/code&gt; must point there. Get that path wrong and the Functions host starts but finds zero functions.&lt;/p&gt;

&lt;p&gt;The final &lt;code&gt;COPY --from=build&lt;/code&gt; pulls the compiled output from the build stage into the runtime image, keeping the SDK and all intermediate build artifacts out of your production container.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you should know before building
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pin your SDK version in &lt;code&gt;global.json&lt;/code&gt;.&lt;/strong&gt; The base image &lt;code&gt;4-dotnet-isolated10.0&lt;/code&gt; bundles a specific .NET 10 runtime. If your local SDK rolls ahead of what the image ships, subtle mismatches at runtime can show up. Pinning keeps builds deterministic across laptops, CI, and the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sdk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.0.201"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rollForward"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"latestPatch"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Package version floor for .NET 10.&lt;/strong&gt; The isolated worker packages below 2.x don't target .NET 10. You need at minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Microsoft.Azure.Functions.Worker&lt;/code&gt; 2.50.0 or later&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Microsoft.Azure.Functions.Worker.Sdk&lt;/code&gt; 2.0.5 or later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're upgrading an existing project from .NET 8, bumping just the &lt;code&gt;TargetFramework&lt;/code&gt; without updating these two packages is the most common failure mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;.NET 10 doesn't run on the Linux Consumption plan.&lt;/strong&gt; This is a hard platform constraint, not a preview gap. If your current app runs on Linux Consumption and you want .NET 10, you need to &lt;a href="https://learn.microsoft.com/azure/azure-functions/migration/migrate-plan-consumption-to-flex" rel="noopener noreferrer"&gt;migrate to the Flex Consumption plan&lt;/a&gt; first. Premium, ACA, and AKS (covered later) all support .NET 10 without this restriction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Functions host runs on .NET 8 internally.&lt;/strong&gt; Even in the &lt;code&gt;dotnet-isolated10.0&lt;/code&gt; image, the host process itself targets .NET 8. Your worker process runs on .NET 10. This is expected behavior for the isolated model, not a bug: the two processes communicate over gRPC, so the runtime versions are independent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;These images are &lt;code&gt;linux/amd64&lt;/code&gt; only.&lt;/strong&gt; If you're on Apple Silicon, Docker Desktop runs them under Rosetta or QEMU emulation. Builds work fine. Performance is noticeably slower than native ARM execution, so keep local integration test suites short.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No slim variant exists for .NET 10 yet.&lt;/strong&gt; The base image is Ubuntu-based (the .NET 10 container images &lt;a href="https://learn.microsoft.com/dotnet/core/compatibility/containers/10.0/default-images-use-ubuntu" rel="noopener noreferrer"&gt;moved from Debian to Ubuntu&lt;/a&gt;), and the full image weighs roughly 1.5 GB. A Mariner-based or distroless option may come later, but as of April 2026, this is what ships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You own base image updates.&lt;/strong&gt; Microsoft publishes monthly security patches to the base images, but unlike managed Functions deployments, custom containers do not auto-update. You pull the latest tag, rebuild, and redeploy. Set a calendar reminder or wire it into your CI pipeline. The &lt;a href="https://learn.microsoft.com/azure/azure-functions/container-concepts#maintaining-custom-containers" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; are explicit about this: maintaining your container is your responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local development with Docker Compose and Azurite
&lt;/h2&gt;

&lt;p&gt;Your function app needs storage. Timer triggers use it for lease management, queue triggers read from it directly, and durable functions store orchestration state there. In production that's an Azure Storage account. Locally, you need &lt;strong&gt;Azurite&lt;/strong&gt;, Microsoft's storage emulator, running alongside your function container.&lt;/p&gt;

&lt;p&gt;Here's the full Docker Compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;azurite&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcr.microsoft.com/azure-storage/azurite&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
      &lt;span class="s"&gt;azurite&lt;/span&gt;
      &lt;span class="s"&gt;--blobHost 0.0.0.0&lt;/span&gt;
      &lt;span class="s"&gt;--queueHost 0.0.0.0&lt;/span&gt;
      &lt;span class="s"&gt;--tableHost 0.0.0.0&lt;/span&gt;
      &lt;span class="s"&gt;--loose&lt;/span&gt;
      &lt;span class="s"&gt;--skipApiVersionCheck&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10000:10000"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10001:10001"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10002:10002"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;azurite-data:/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nc -z 127.0.0.1 &lt;/span&gt;&lt;span class="m"&gt;10000&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;

  &lt;span class="na"&gt;functions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;AzureWebJobsStorage=DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;QueueEndpoint=http://azurite:10001/devstoreaccount1;TableEndpoint=http://azurite:10002/devstoreaccount1&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;azurite&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;azurite-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--blobHost 0.0.0.0&lt;/code&gt; flags (and their queue/table equivalents) tell Azurite to listen on all network interfaces, not just localhost. Without them, your function container can't reach Azurite across the Docker network. &lt;code&gt;--loose&lt;/code&gt; relaxes strict API validation. &lt;code&gt;--skipApiVersionCheck&lt;/code&gt; prevents version mismatch errors when the Functions runtime targets a newer Storage API than Azurite supports.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;named volume&lt;/strong&gt; &lt;code&gt;azurite-data&lt;/code&gt; keeps your storage data intact between &lt;code&gt;docker compose down&lt;/code&gt; and &lt;code&gt;docker compose up&lt;/code&gt;. Queue messages, blob uploads, table entities: all survive restarts. Drop the volume only when you want a clean slate (&lt;code&gt;docker compose down -v&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;health check&lt;/strong&gt; deserves attention. Without it, Docker starts both containers simultaneously. Your function app boots in seconds, tries to connect to Azurite, and fails because Azurite hasn't finished initializing. The &lt;code&gt;nc -z 127.0.0.1 10000&lt;/code&gt; check confirms Azurite is actually accepting connections before the function container starts.&lt;/p&gt;

&lt;p&gt;Now for the part that will cost you an hour if you don't know about it.&lt;/p&gt;

&lt;p&gt;Your first instinct for the storage connection string will be &lt;code&gt;UseDevelopmentStorage=true&lt;/code&gt;. That's what every Azure Functions tutorial uses, and it works fine when Azurite runs on your host machine. Inside Docker, it breaks. The shorthand expands to endpoints pointing at &lt;code&gt;127.0.0.1&lt;/code&gt;, which inside the function container means "myself," not "the Azurite container next door."&lt;/p&gt;

&lt;p&gt;The fix is the explicit connection string you see in the Compose file above. The critical difference: every endpoint URL uses &lt;code&gt;azurite&lt;/code&gt; as the hostname (the Compose service name) instead of &lt;code&gt;127.0.0.1&lt;/code&gt;. Docker's internal DNS resolves &lt;code&gt;azurite&lt;/code&gt; to the correct container IP automatically. The account name and key are Azurite's well-known development credentials, the same ones &lt;code&gt;UseDevelopmentStorage=true&lt;/code&gt; uses under the hood.&lt;/p&gt;

&lt;p&gt;One practical tip: that connection string is long and ugly. Don't try to split it across multiple lines in your Compose file or inject it from a &lt;code&gt;.env&lt;/code&gt; file with line breaks. YAML will quietly mangle it. Keep it on a single line, or use a &lt;code&gt;.env&lt;/code&gt; file with the entire value on one line and reference it with &lt;code&gt;${AzureWebJobsStorage}&lt;/code&gt; in your Compose file.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;docker compose up --build&lt;/code&gt; and you should see Azurite report all three services listening, followed by your function app discovering its triggers. If the function container restarts in a loop, check the connection string first. Nine times out of ten, that's the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging in containers: VS Code and Rider
&lt;/h2&gt;

&lt;p&gt;Add a debug stage to your Dockerfile that installs the .NET debugger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;debug&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--tool-path&lt;/span&gt; /tools dotnet-dump
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    curl unzip procps &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://aka.ms/getvsdbgsh | bash /dev/stdin &lt;span class="nt"&gt;-v&lt;/span&gt; latest &lt;span class="nt"&gt;-l&lt;/span&gt; /vsdbg &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get clean &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; DOTNET_USE_POLLING_FILE_WATCHER=1&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["dotnet", "run", "--project", "/src/HttpTriggerDemo"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;DOTNET_USE_POLLING_FILE_WATCHER&lt;/code&gt; environment variable is required because Docker volume mounts don't support &lt;code&gt;inotify&lt;/code&gt;. Without it, file change detection silently fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  VS Code with pipeTransport
&lt;/h3&gt;

&lt;p&gt;Point your &lt;code&gt;launch.json&lt;/code&gt; at the container using &lt;strong&gt;pipeTransport&lt;/strong&gt; instead of opening a debug port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Attach to Docker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"coreclr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"attach"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"processId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${command:pickRemoteProcess}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pipeTransport"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pipeProgram"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pipeArgs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"exec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-functions-debug"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"debuggerPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/vsdbg/vsdbg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pipeCwd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceFileMap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"/src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pipeTransport&lt;/code&gt; sends debug commands through &lt;code&gt;docker exec&lt;/code&gt;, so you never expose a debug port. The &lt;code&gt;sourceFileMap&lt;/code&gt; entry maps the container's &lt;code&gt;/src&lt;/code&gt; path back to your workspace so breakpoints resolve correctly. Start the container, hit F5 in VS Code, pick the &lt;code&gt;dotnet&lt;/code&gt; process, and you're attached.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rider
&lt;/h3&gt;

&lt;p&gt;Rider handles most of this automatically. Open &lt;strong&gt;Run &amp;gt; Attach to Process&lt;/strong&gt;, select the &lt;strong&gt;Docker&lt;/strong&gt; tab, and pick your container. Rider installs its own debug agent on first attach. If you use Docker Compose, Rider also supports a native &lt;strong&gt;Docker Compose&lt;/strong&gt; run configuration that builds, starts, and attaches in one step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Compose debug profile
&lt;/h3&gt;

&lt;p&gt;Separate your debug configuration using a &lt;strong&gt;Compose profile&lt;/strong&gt; so it doesn't interfere with production builds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;functions-debug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;debug&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./src:/src&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DOTNET_USE_POLLING_FILE_WATCHER=1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;AzureWebJobsStorage=DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;QueueEndpoint=http://azurite:10001/devstoreaccount1;TableEndpoint=http://azurite:10002/devstoreaccount1&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;azurite&lt;/span&gt;
    &lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;debug&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with &lt;code&gt;docker compose --profile debug up&lt;/code&gt;. The &lt;code&gt;target: debug&lt;/code&gt; directive tells Compose to stop at your debug stage, which includes the SDK and &lt;code&gt;vsdbg&lt;/code&gt; but skips the production publish step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hot reload: set expectations
&lt;/h3&gt;

&lt;p&gt;Using &lt;code&gt;dotnet watch&lt;/code&gt; to wrap &lt;code&gt;func start&lt;/code&gt; inside the container works, but every code change triggers a full restart. Expect 4-6 second cycles. That's usable for occasional debugging sessions, not for rapid iteration.&lt;/p&gt;

&lt;p&gt;The pragmatic split: run &lt;code&gt;func start&lt;/code&gt; on your host machine for day-to-day development. Keep Azurite and any dependencies (Service Bus emulator, CosmosDB emulator) in Docker. Reserve full-container debugging for integration testing or reproducing environment-specific issues. You get fast inner-loop feedback without giving up the production-parity benefits of containerized dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to deploy: ACA vs Premium vs AKS
&lt;/h2&gt;

&lt;p&gt;You have a containerized Function. Now you need somewhere to run it. Three options exist, and each makes a different trade-off between operational control and managed convenience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure Container Apps (ACA)
&lt;/h3&gt;

&lt;p&gt;ACA is the recommended default for containerized Azure Functions. The platform reads your Function triggers and configures KEDA scaling rules automatically, so you never write ScaledObject YAML yourself.&lt;/p&gt;

&lt;p&gt;Deploy with the Azure CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az containerapp create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-functions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; my-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--environment&lt;/span&gt; my-env &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; myregistry.azurecr.io/my-functions:latest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--registry-server&lt;/span&gt; myregistry.azurecr.io &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ingress&lt;/span&gt; external &lt;span class="nt"&gt;--target-port&lt;/span&gt; 80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--min-replicas&lt;/span&gt; 0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-replicas&lt;/span&gt; 30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;--min-replicas 0&lt;/code&gt; and your app scales to zero when idle, meaning zero compute cost during quiet periods.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; follows the Container Apps model. On the Consumption plan, you pay per vCPU-second and GiB-second, with a monthly free grant of 180,000 vCPU-seconds and 360,000 GiB-seconds per subscription. For a Function that processes a few thousand events per day and idles overnight, you could land under $5/month. Dedicated workload profiles are available if you need guaranteed compute or GPU access, billed per instance rather than per resource consumed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cold start&lt;/strong&gt; is the main gotcha. When your app scales from zero, the platform needs to pull the container image, provision resources, and start the Functions host. For a typical .NET isolated Function, teams commonly report 5-15 seconds on the first request after an idle period (Microsoft doesn't publish official cold start numbers). You can eliminate this by setting &lt;code&gt;--min-replicas 1&lt;/code&gt;, but that means you pay for at least one instance around the clock. Keeping your container image small (pin to a specific tag, avoid unnecessary layers) helps reduce cold start time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What ACA does not support:&lt;/strong&gt; deployment slots, Functions access keys via the portal, and Functions proxies. If you rely on staging slots for zero-downtime swaps, you'll need to use ACA's built-in blue-green deployment with traffic splitting instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure Functions Premium Plan
&lt;/h3&gt;

&lt;p&gt;The Premium plan (Elastic Premium, SKUs starting with &lt;code&gt;EP&lt;/code&gt;) is the original way to run custom containers in Azure Functions. It predates ACA and still has one killer feature: &lt;strong&gt;always-ready instances with prewarmed buffers&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az functionapp plan create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; my-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-premium-plan &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; eastus &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sku&lt;/span&gt; EP1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--is-linux&lt;/span&gt;

az functionapp create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; my-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--plan&lt;/span&gt; my-premium-plan &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-functions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--deployment-container-image-name&lt;/span&gt; myregistry.azurecr.io/my-functions:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three SKU sizes are available:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr6vvifjh9f1b528k9pu5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr6vvifjh9f1b528k9pu5.png" alt="Premium plan SKU sizes: EP1 (1 vCPU, 3.5 GB), EP2 (2 vCPUs, 7 GB), EP3 (4 vCPUs, 14 GB)" width="346" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The billing model is the critical difference from ACA.&lt;/strong&gt; Premium plan charges per core-second and memory across all allocated instances, with no execution charge. At least one instance must always be running. An EP1 instance running 24/7 costs roughly $155-175/month (varies by region). You cannot scale to zero. That always-on instance is the price you pay for eliminating cold starts entirely.&lt;/p&gt;

&lt;p&gt;Where the Premium plan shines is latency-sensitive HTTP traffic. When load spikes, prewarmed instances are already initialized and waiting. No container pull, no cold start. For Functions that must respond in under 200ms consistently, this matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out for the SKU naming confusion.&lt;/strong&gt; &lt;code&gt;EP1&lt;/code&gt; is Elastic Premium (dynamic scaling). &lt;code&gt;P1V2&lt;/code&gt; is a Dedicated App Service plan (no dynamic scaling). Pick the wrong one and you'll pay more for less flexibility.&lt;/p&gt;

&lt;p&gt;Maximum scale-out is up to 100 instances. The default &lt;code&gt;maximumElasticWorkerCount&lt;/code&gt; in ARM templates is 20, so you may need to raise that limit explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  AKS with KEDA
&lt;/h3&gt;

&lt;p&gt;If your team already operates a Kubernetes cluster, running Functions there avoids introducing a new compute platform. You install KEDA as an AKS add-on, deploy your Function container as a standard Kubernetes deployment, and KEDA handles scaling based on event triggers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keda.sh/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ScaledObject&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-functions-scaler&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scaleTargetRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-functions&lt;/span&gt;
  &lt;span class="na"&gt;minReplicaCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="na"&gt;maxReplicaCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
  &lt;span class="na"&gt;triggers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure-servicebus&lt;/span&gt;
      &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;queueName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders&lt;/span&gt;
        &lt;span class="na"&gt;messageCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5"&lt;/span&gt;
      &lt;span class="na"&gt;authenticationRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;servicebus-auth&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;KEDA supports these Azure Functions triggers directly: Azure Storage Queues, Azure Service Bus, Azure Event Hubs / IoT Hubs, Apache Kafka, and RabbitMQ. HTTP triggers work, but KEDA does not manage them directly; you configure HTTP scaling through the Horizontal Pod Autoscaler or Container Apps' HTTP scaler instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the only option that is community-supported, not Microsoft-supported.&lt;/strong&gt; The docs are explicit: "Best-effort support is provided by contributors and from the community." If something breaks at 2am, you're opening a GitHub issue, not filing a support ticket. You also own the full Kubernetes stack: node pools, networking, RBAC, upgrades, monitoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost&lt;/strong&gt; depends entirely on your cluster. If you're already paying for AKS nodes, adding a Function container is effectively free at the compute layer. If you'd be spinning up a new cluster just for Functions, the minimum AKS cost (one node with a Standard_D2s_v3 VM) starts around $70/month before you've deployed anything. KEDA itself is free and runs as a lightweight deployment in your cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cold start&lt;/strong&gt; on AKS matches whatever your cluster can provision. With KEDA's scale-to-zero, a cold start involves scheduling a pod, pulling the image (if not cached), and starting the container. On a warm cluster with cached images, that's 3-10 seconds. On a cluster that needs to scale up a node, it could be 2-4 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trade-offs at a glance
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fap8kfyjh0ly2s5f8v0su.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fap8kfyjh0ly2s5f8v0su.png" alt="Trade-offs comparison: ACA vs Premium Plan vs AKS with KEDA" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The decision tree is short.&lt;/strong&gt; If you don't already run Kubernetes, don't start now for a single Function app. If your Function handles latency-sensitive HTTP requests and cold starts are unacceptable, use the Premium plan and accept the always-on cost. For everything else, ACA with the Consumption plan gives you scale-to-zero, automatic KEDA configuration, and the lowest operational overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Docker adds value
&lt;/h2&gt;

&lt;p&gt;The deployment choice assumes the container was worth building in the first place. Every custom container you ship is infrastructure you now own: a registry to manage, a base image to patch monthly, a CI pipeline stage that didn't exist before. Zip-deploy skips all of that. Microsoft patches the managed host, and you never think about it.&lt;/p&gt;

&lt;p&gt;That trade-off only flips when the managed host can't do what your function requires. Puppeteer needs Chromium installed at the OS level. Your compliance team mandates identical images from laptop to production. Your platform team already runs everything on AKS and adding a second deployment model would create more problems than it solves. Those are real constraints, not preferences.&lt;/p&gt;

&lt;p&gt;The setup cost is lower than it looks. Twelve lines of Dockerfile, a Compose file with Azurite, and one container image that deploys to ACA, Premium, or AKS without changes. The ongoing cost is the part that matters: monthly base image pulls, rebuild-and-redeploy cycles, and one more thing to monitor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is your function broken without OS-level control, or would zip-deploy work fine if you tried it first?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>dotnet</category>
      <category>docker</category>
      <category>azurefunctions</category>
    </item>
    <item>
      <title>Production Realities: When Azure Functions Stops Being Serverless</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 10 Apr 2026 05:38:43 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g</link>
      <guid>https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g</guid>
      <description>&lt;h2&gt;
  
  
  The Enterprise Reality Check
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  VNet: The Requirement That Changes Everything
&lt;/h2&gt;

&lt;p&gt;Most enterprise environments mandate &lt;strong&gt;VNet integration&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;These are your options (East US pricing, April 2026):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;VNet&lt;/th&gt;
&lt;th&gt;Scale to Zero&lt;/th&gt;
&lt;th&gt;Min Idle Cost/mo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Consumption&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flex Consumption (on-demand)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flex Consumption (1 always-ready, 2 GB)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;~$21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium EP1&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;~$146&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium EP2&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;~$291&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dedicated S1/P1v3&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;varies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container Apps (1 replica, 0.25 vCPU)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;~$10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flex Consumption&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;But Flex has real constraints. Before you commit to it, compare what you're giving up:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;th&gt;Flex Consumption&lt;/th&gt;
&lt;th&gt;Premium&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OS&lt;/td&gt;
&lt;td&gt;Linux only&lt;/td&gt;
&lt;td&gt;Windows + Linux&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apps per plan&lt;/td&gt;
&lt;td&gt;One&lt;/td&gt;
&lt;td&gt;Multiple&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment slots&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Up to 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-process .NET&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App init timeout&lt;/td&gt;
&lt;td&gt;Fixed 30s&lt;/td&gt;
&lt;td&gt;No limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NFS file shares&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regional availability&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Broad&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;one-app-per-plan&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;30-second app init timeout&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;If your codebase uses &lt;strong&gt;in-process .NET&lt;/strong&gt; (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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Plan Escalation Path
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Then the requirements start arriving, one at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  VNet integration
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Running total: $0/mo idle&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Private endpoints
&lt;/h3&gt;

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

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

&lt;p&gt;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 &lt;strong&gt;~$22/mo&lt;/strong&gt; for storage endpoints.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running total: ~$29/mo (Flex on-demand)&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The fork: when Premium becomes unavoidable
&lt;/h3&gt;

&lt;p&gt;Most teams can stay on Flex through the VNet and private endpoint requirements. But Flex has constraints that force some teams onto &lt;strong&gt;Premium EP1&lt;/strong&gt;:&lt;/p&gt;

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

&lt;p&gt;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 = &lt;strong&gt;~$146/mo&lt;/strong&gt;. It runs 24/7 whether your function executes or not. Storage private endpoints on Premium cost &lt;strong&gt;~$30/mo&lt;/strong&gt; (four endpoints, including Azure Files).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running total if forced to Premium: ~$176/mo&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cold starts
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running total: ~$50/mo (Flex + always-ready) or ~$176/mo (Premium)&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  API Management
&lt;/h3&gt;

&lt;p&gt;Your API needs rate limiting and a developer portal. You add &lt;strong&gt;Azure API Management&lt;/strong&gt;. The pricing depends on what your organization needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;APIM Basic (classic)&lt;/strong&gt;: ~$147/mo, no VNet integration, 99.95% SLA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APIM Standard v2&lt;/strong&gt;: ~$700/mo, partial VNet support (backend only), 99.95% SLA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APIM Premium (classic)&lt;/strong&gt;: ~$2,795/mo, full VNet integration, 99.99% SLA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most teams start with Basic and accept the VNet gap. Some compliance requirements force Standard v2 or higher.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running total: ~$197/mo (Flex + Basic) or ~$323/mo (Premium + Basic)&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  WAF protection
&lt;/h3&gt;

&lt;p&gt;Compliance also wants a Web Application Firewall in front of your API. You deploy &lt;strong&gt;Application Gateway WAF_v2&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;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 &lt;strong&gt;~$333-335/mo&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Application Gateway v1 retires April 28, 2026, so WAF_v2 is the only option going forward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running total: ~$530/mo (Flex floor) or ~$656/mo (Premium floor)&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The full picture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build and Deploy Friction
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;az functionapp update --sku FC1&lt;/code&gt;. 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  The app settings cleanup
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;These settings must be removed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;h3&gt;
  
  
  Infrastructure as Code breaks across plans
&lt;/h3&gt;

&lt;p&gt;Your Terraform and Bicep templates don't just need new property values. They need different resources entirely.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Consumption / Premium: this resource&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_linux_function_app"&lt;/span&gt; &lt;span class="s2"&gt;"func"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;service_plan_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_service_plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Flex Consumption: different resource, different schema&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_function_app_flex_consumption"&lt;/span&gt; &lt;span class="s2"&gt;"func"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;service_plan_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_service_plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;site_config&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nx"&gt;storage_container_type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"blobContainer"&lt;/span&gt;
  &lt;span class="nx"&gt;storage_container_endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${azurerm_storage_account.sa.primary_blob_endpoint}${azurerm_storage_container.deploy.name}"&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;In Bicep, the SKU values map to different tiers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;SKU Name&lt;/th&gt;
&lt;th&gt;SKU Tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Consumption&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Y1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Dynamic&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flex Consumption&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FC1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FlexConsumption&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;EP1&lt;/code&gt;/&lt;code&gt;EP2&lt;/code&gt;/&lt;code&gt;EP3&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ElasticPremium&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;FC1&lt;/code&gt; plan also requires &lt;code&gt;reserved: true&lt;/code&gt; and a &lt;code&gt;functionAppConfig&lt;/code&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI/CD pipeline adjustments
&lt;/h3&gt;

&lt;p&gt;GitHub Actions requires the &lt;code&gt;sku&lt;/code&gt; parameter in &lt;code&gt;azure/functions-action&lt;/code&gt; when deploying to Flex Consumption with a publish profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Azure/functions-action@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.FUNCTION_APP_NAME }}&lt;/span&gt;
    &lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.PACKAGE_PATH }}&lt;/span&gt;
    &lt;span class="na"&gt;publish-profile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PUBLISH_PROFILE }}&lt;/span&gt;
    &lt;span class="na"&gt;sku&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;flexconsumption'&lt;/span&gt;
    &lt;span class="na"&gt;remote-build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;sku: 'flexconsumption'&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;scm-do-build-during-deployment&lt;/code&gt; and &lt;code&gt;enable-oryx-build&lt;/code&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Private endpoints break GitHub-hosted runners
&lt;/h3&gt;

&lt;p&gt;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 &lt;code&gt;Failed to fetch Kudu App Settings (CODE: 404)&lt;/code&gt; or a 401, and the error message gives you almost no indication that networking is the problem.&lt;/p&gt;

&lt;p&gt;Your options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted runner inside the VNet&lt;/strong&gt;: works, but now you're maintaining a VM ($50-100/month) to deploy a "serverless" function&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub-hosted runners with Azure private networking&lt;/strong&gt;: 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy via ARM using a service principal&lt;/strong&gt;: bypasses SCM entirely, pushes configuration through the Azure Resource Manager API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage to blob storage&lt;/strong&gt;: upload your package to a storage account the function app can reach, then trigger deployment from there&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On Premium, you also need &lt;code&gt;WEBSITE_SKIP_CONTENTSHARE_VALIDATION=1&lt;/code&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  The compound effect
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The real cost of plan migration shows up in the sprint consumed by infrastructure work, not in the Azure bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Serverless Stops Making Sense
&lt;/h2&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;p&gt;Signs it's time to look elsewhere:&lt;/p&gt;

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

&lt;p&gt;The two alternatives worth evaluating are Azure Container Apps and App Service. They solve different problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure Container Apps: the container-native path
&lt;/h3&gt;

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

&lt;p&gt;The resource definition looks like a normal container app deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az containerapp create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-func-app &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; rg-prod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--environment&lt;/span&gt; my-aca-env &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; myregistry.azurecr.io/my-func:latest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--kind&lt;/span&gt; functionapp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--min-replicas&lt;/span&gt; 0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-replicas&lt;/span&gt; 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;KEDA&lt;/strong&gt; (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.&lt;/p&gt;

&lt;p&gt;What you gain over Premium Functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sidecar containers&lt;/strong&gt;: run log forwarders, auth proxies, or Dapr sidecars alongside your function app in the same pod&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dapr integration&lt;/strong&gt;: pub/sub, state management, and service invocation without managing the infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic splitting via revisions&lt;/strong&gt;: route a percentage of traffic to a new version before promoting it, something Functions deployment slots can't do with the same granularity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU support&lt;/strong&gt;: if you're running inference workloads alongside event-driven functions, ACA supports GPU-backed workload profiles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you give up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Containerization is mandatory&lt;/strong&gt;. 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No built-in continuous deployment&lt;/strong&gt; from the Functions tooling. You wire up GitHub Actions or Azure Pipelines yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inbound Private Endpoints through the Functions networking layer are not available.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  App Service: the predictable option
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;App Service&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Picking the right exit
&lt;/h3&gt;

&lt;p&gt;The choice depends on what pushed you away from Functions in the first place:&lt;/p&gt;

&lt;p&gt;If your main frustration is &lt;strong&gt;cost&lt;/strong&gt;, 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.&lt;/p&gt;

&lt;p&gt;If your frustration is &lt;strong&gt;operational control&lt;/strong&gt;, 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.&lt;/p&gt;

&lt;p&gt;Teams frustrated by &lt;strong&gt;complexity for a steady workload&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making an Honest Choice
&lt;/h2&gt;

&lt;p&gt;Three plans, three different products.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumption&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flex Consumption&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Premium&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Has a security review pushed you from Consumption to Premium, or did you start on Premium from day one?&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bonus: Production Realities: When Serverless Stops Being Serverless (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>dotnet</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Azure Functions Observability: From Blind Spots to Production Clarity</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 03 Apr 2026 06:25:08 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4</link>
      <guid>https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4</guid>
      <description>&lt;p&gt;Your function works locally, passes all tests, and deploys without errors. But how do you know it's healthy at 2am when a queue-triggered function silently drops messages? With a traditional web app on a VM, you'd SSH in, check logs, inspect process health. Serverless strips all of that away.&lt;/p&gt;

&lt;p&gt;The observability gap in serverless is real, and it's structural. Your function runs inside an ephemeral container that spins up on demand, processes an event, and disappears. There's no persistent server to monitor, no process to attach a debugger to, no &lt;code&gt;/var/log&lt;/code&gt; to tail. When your function app scales to zero between invocations, even continuous metric collection breaks down: there is literally nothing running to emit telemetry.&lt;/p&gt;

&lt;p&gt;And when it scales from zero to fifty concurrent instances under load, correlating a single failed request across that distributed execution becomes a different problem entirely. Traditional APM tools assume long-lived processes with stable identities. Serverless functions violate every one of those assumptions.&lt;/p&gt;

&lt;p&gt;Application Insights fills that gap. When connected to your function app (via a connection string, not the deprecated instrumentation key), it automatically captures request telemetry for every function execution, tracks dependencies like HTTP calls and database queries, collects host-level performance counters, and aggregates invocation metrics you can query from the portal or through code.&lt;/p&gt;

&lt;p&gt;On top of that, it gives you structured log queries with KQL (Kusto Query Language), an application map that visualizes how your function calls downstream services, distributed tracing that follows a single request across multiple functions and dependencies, and alerting rules that can page you before your users notice something is wrong.&lt;/p&gt;

&lt;p&gt;The examples below use the classic Application Insights SDK with the isolated worker model, which is what most production .NET function apps run today. The companion repository at &lt;a href="https://github.com/MO2k4/azure-functions-samples" rel="noopener noreferrer"&gt;azure-functions-samples&lt;/a&gt; has working examples of both the classic SDK (&lt;code&gt;HttpTriggerDemo&lt;/code&gt;) and OpenTelemetry (&lt;code&gt;EventHubDemo&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Application Insights
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Creating the Resource
&lt;/h3&gt;

&lt;p&gt;You can create an Application Insights resource through the Azure Portal (search "Application Insights" and click Create) or provision it with Bicep alongside your function app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: 'appi-orders-prod'
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    WorkspaceResourceId: logAnalyticsWorkspace.id
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resource gives you a &lt;strong&gt;connection string&lt;/strong&gt;, which looks like &lt;code&gt;InstrumentationKey=&amp;lt;guid&amp;gt;;IngestionEndpoint=https://region.in.applicationinsights.azure.com/&lt;/code&gt;. Use this, not the instrumentation key alone. Microsoft deprecated standalone instrumentation key ingestion in March 2025, and connection strings are required for sovereign clouds, regional endpoints, and Entra ID-authenticated ingestion. Store the value in your function app's &lt;code&gt;APPLICATIONINSIGHTS_CONNECTION_STRING&lt;/code&gt; application setting, and Azure picks it up automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  NuGet Packages
&lt;/h3&gt;

&lt;p&gt;The isolated worker model needs two packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Microsoft.ApplicationInsights.WorkerService &lt;span class="nt"&gt;--version&lt;/span&gt; 2.22.0
dotnet add package Microsoft.Azure.Functions.Worker.ApplicationInsights &lt;span class="nt"&gt;--version&lt;/span&gt; 2.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pin &lt;code&gt;Microsoft.ApplicationInsights.WorkerService&lt;/code&gt; to 2.22.0.&lt;/strong&gt; Version 3.0.0 migrated to OpenTelemetry internally and broke the &lt;code&gt;ITelemetryInitializer&lt;/code&gt; interface that &lt;code&gt;Microsoft.Azure.Functions.Worker.ApplicationInsights&lt;/code&gt; depends on. The result is a &lt;code&gt;TypeLoadException&lt;/code&gt; at startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.TypeLoadException: Could not load type
'Microsoft.ApplicationInsights.Extensibility.ITelemetryInitializer'
from assembly 'Microsoft.ApplicationInsights, Version=3.0.0.1'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This affects every .NET version (not just .NET 10). Until the Functions worker package ships a compatible update, stay on 2.22.0. Add a comment in your &lt;code&gt;.csproj&lt;/code&gt; so the next person who runs &lt;code&gt;dotnet outdated&lt;/code&gt; doesn't blindly upgrade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Do NOT upgrade to 3.x: breaks Functions worker. See github.com/Azure/azure-functions-dotnet-worker/issues/3322 --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.ApplicationInsights.WorkerService"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.22.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.ApplicationInsights"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.0.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- Check https://github.com/Azure/azure-functions-dotnet-worker/issues/3322 before upgrading either package --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second package, &lt;code&gt;Microsoft.Azure.Functions.Worker.ApplicationInsights&lt;/code&gt;, is what connects your dependency telemetry (HTTP calls, SQL queries, queue operations) back to the parent function invocation. Without it, correlation breaks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Program.cs Configuration
&lt;/h3&gt;

&lt;p&gt;Two method calls handle the setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker.Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Hosting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddApplicationInsightsTelemetryWorkerService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsApplicationInsights&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AddApplicationInsightsTelemetryWorkerService()&lt;/code&gt; registers the Application Insights SDK for worker-style apps (background services, Functions). &lt;code&gt;ConfigureFunctionsApplicationInsights()&lt;/code&gt; hooks into the Functions runtime's activity pipeline so that incoming triggers and outbound calls produce the right request and dependency telemetry.&lt;/p&gt;

&lt;p&gt;See this in context in the &lt;a href="https://github.com/MO2k4/azure-functions-samples/blob/main/HttpTriggerDemo/Program.cs" rel="noopener noreferrer"&gt;HttpTriggerDemo Program.cs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One catch: the SDK registers a default logging filter that suppresses everything below &lt;code&gt;Warning&lt;/code&gt;. If you leave it in place, your &lt;code&gt;ILogger.LogInformation()&lt;/code&gt; calls never reach Application Insights. Remove it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker.Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Hosting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Logging&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddApplicationInsightsTelemetryWorkerService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsApplicationInsights&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LoggerFilterOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;LoggerFilterRule&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;defaultRule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProviderName&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt;
            &lt;span class="s"&gt;"Microsoft.Extensions.Logging.ApplicationInsights.ApplicationInsightsLoggerProvider"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultRule&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultRule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, manage log levels through &lt;code&gt;appsettings.json&lt;/code&gt; (loaded automatically by &lt;code&gt;FunctionsApplication.CreateBuilder&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Microsoft"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ApplicationInsights"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What Gets Auto-Collected vs. What You Add Manually
&lt;/h3&gt;

&lt;p&gt;Once that's in place, the SDK and the Functions runtime collect telemetry automatically, with no extra code:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08iqaljtc0amwpqfevt9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08iqaljtc0amwpqfevt9.png" alt="Auto-collected vs manual telemetry" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha: Two Log Pipelines, Two Configurations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The isolated worker model runs your code in a separate process from the Functions host. This means &lt;code&gt;host.json&lt;/code&gt; controls logging for the &lt;strong&gt;host process&lt;/strong&gt; (trigger dispatch, scaling decisions, extension lifecycle), while your &lt;code&gt;Program.cs&lt;/code&gt; or &lt;code&gt;appsettings.json&lt;/code&gt; controls logging for the &lt;strong&gt;worker process&lt;/strong&gt; (your function code, your dependencies, your &lt;code&gt;ILogger&lt;/code&gt; calls). If you set &lt;code&gt;"Default": "Information"&lt;/code&gt; in &lt;code&gt;host.json&lt;/code&gt; but never configure your worker, your application logs still default to &lt;code&gt;Warning&lt;/code&gt; only. You have to configure both sides, and they use different files.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Logging Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Structured Logging with ILogger
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Structured logging&lt;/strong&gt; writes log entries as key-value pairs instead of flat strings. In Application Insights, those keys become columns in the &lt;code&gt;customDimensions&lt;/code&gt; property of the &lt;code&gt;traces&lt;/code&gt; table, which means you can filter and aggregate by them in KQL without parsing text.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ILogger&lt;/code&gt; API supports this through &lt;strong&gt;message templates&lt;/strong&gt;: named placeholders wrapped in curly braces, filled by positional arguments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProcessOrderFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProcessOrderFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessOrderFunction&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;QueueTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;OrderMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Order received: {OrderId} from customer {CustomerId}, total {OrderTotal}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stopwatch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartNew&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ProcessAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Order {OrderId} processed successfully in {ElapsedMs}ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ElapsedMilliseconds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to watch here. First, use &lt;strong&gt;PascalCase&lt;/strong&gt; for placeholder names (&lt;code&gt;{OrderId}&lt;/code&gt;, not &lt;code&gt;{orderId}&lt;/code&gt; or &lt;code&gt;{order_id}&lt;/code&gt;). Application Insights stores these as &lt;code&gt;customDimensions&lt;/code&gt; keys, and PascalCase matches the convention for the rest of the telemetry schema. Second, never use string interpolation (&lt;code&gt;$"Order {orderId}"&lt;/code&gt;) in log calls. Interpolated strings defeat structured logging entirely: the provider receives a pre-formatted string with no queryable fields, and the arguments are evaluated even when the log level is disabled.&lt;/p&gt;

&lt;p&gt;In the Application Insights &lt;code&gt;traces&lt;/code&gt; table, a query like this pulls all logs for a specific order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;traces
| where customDimensions.OrderId == "ORD-20260330-1847"
| project timestamp, message, customDimensions.CustomerId, customDimensions.ElapsedMs
| order by timestamp asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  High-Performance Logging with Source Generators
&lt;/h3&gt;

&lt;p&gt;For functions processing thousands of messages per second (high-throughput queue or Event Hub triggers), the standard &lt;code&gt;LogInformation&lt;/code&gt; extension methods have measurable overhead: they box value-type arguments, allocate a &lt;code&gt;params object[]&lt;/code&gt; on every call, and parse the message template at runtime.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;&lt;code&gt;[LoggerMessage]&lt;/code&gt; source generator&lt;/strong&gt; eliminates all three costs by generating strongly typed methods at compile time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderLogs&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;LoggerMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;EventId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Order {OrderId} received from {CustomerId}, total {OrderTotal}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OrderReceived&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;orderTotal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;LoggerMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;EventId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Order {OrderId} retry attempt {RetryCount}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OrderRetrying&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call these directly: &lt;code&gt;OrderLogs.OrderReceived(logger, message.OrderId, message.CustomerId, message.Total)&lt;/code&gt;. The generated code includes a log-level check before evaluating any arguments, so you pay zero cost when Information-level logging is disabled in production. Use this pattern on hot paths; for functions running a few times per minute, the standard extension methods are fine. The companion repo has source generator examples in both &lt;a href="https://github.com/MO2k4/azure-functions-samples/blob/main/HttpTriggerDemo/Logging/OrderLogs.cs" rel="noopener noreferrer"&gt;HttpTriggerDemo/Logging/OrderLogs.cs&lt;/a&gt; and &lt;a href="https://github.com/MO2k4/azure-functions-samples/blob/main/EventHubDemo/Logging/SensorLogs.cs" rel="noopener noreferrer"&gt;EventHubDemo/Logging/SensorLogs.cs&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Correlation with BeginScope
&lt;/h3&gt;

&lt;p&gt;Individual log lines tell you what happened; &lt;strong&gt;BeginScope&lt;/strong&gt; ties related entries into a single operation. When you wrap your function body in a scope, every log entry inside it automatically inherits the scope's properties as &lt;code&gt;customDimensions&lt;/code&gt; in Application Insights.&lt;/p&gt;

&lt;p&gt;The critical detail: you must pass a &lt;code&gt;Dictionary&amp;lt;string, object&amp;gt;&lt;/code&gt; to &lt;code&gt;BeginScope&lt;/code&gt; for the keys to appear as individual &lt;code&gt;customDimensions&lt;/code&gt; columns. A plain string or a message template with arguments produces a single formatted string in the scope, which is much harder to query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessOrderFunction&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;QueueTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;OrderMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"OrderId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"CustomerId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"TenantId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TenantId&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Validating order"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ValidateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Charging payment"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ChargePaymentAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order complete"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every log line inside that &lt;code&gt;using&lt;/code&gt; block now carries &lt;code&gt;OrderId&lt;/code&gt;, &lt;code&gt;CustomerId&lt;/code&gt;, and &lt;code&gt;TenantId&lt;/code&gt; in its &lt;code&gt;customDimensions&lt;/code&gt;, even the ones from &lt;code&gt;ValidateAsync&lt;/code&gt; and &lt;code&gt;ChargePaymentAsync&lt;/code&gt; (assuming they use the same &lt;code&gt;ILogger&lt;/code&gt; instance). This is how you trace a complete business operation across multiple internal methods without threading correlation IDs through every method signature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Log Levels for Production
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;host.json&lt;/code&gt; &lt;strong&gt;logLevel&lt;/strong&gt; section controls which categories reach Application Insights. Two categories are easy to misconfigure, and getting them wrong silently breaks your monitoring dashboards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"logLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Function"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Host.Results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Host.Aggregator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Trace"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;Host.Results&lt;/code&gt;&lt;/strong&gt; feeds the &lt;code&gt;requests&lt;/code&gt; table. If you raise this above &lt;code&gt;Information&lt;/code&gt;, successful function executions stop appearing in the Application Insights Performance and Failures blades, and the Function Monitor tab in the portal goes blank. You lose your primary visibility into whether functions are running at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Host.Aggregator&lt;/code&gt;&lt;/strong&gt; feeds the &lt;code&gt;customMetrics&lt;/code&gt; table with aggregated counts and durations. Set it to &lt;code&gt;Trace&lt;/code&gt; so the runtime writes every batch. If you raise this to &lt;code&gt;Warning&lt;/code&gt; or higher, the function overview dashboard in the portal loses its success rate and duration charts.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;default: Warning&lt;/code&gt; baseline keeps noise low for framework categories (&lt;code&gt;Microsoft.*&lt;/code&gt;, &lt;code&gt;Worker&lt;/code&gt;, &lt;code&gt;System.*&lt;/code&gt;) while &lt;code&gt;Function: Information&lt;/code&gt; ensures your own function logs reach Application Insights.&lt;/p&gt;

&lt;p&gt;When you need to change log levels without redeploying, override any &lt;code&gt;host.json&lt;/code&gt; value through app settings. The pattern replaces dots with double underscores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;AzureFunctionsJobHost__logging__logLevel__Function.ProcessOrder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Debug&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This takes effect on the next function host restart (which happens automatically when you update an app setting) and lets you temporarily increase verbosity for a single function without touching &lt;code&gt;host.json&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sampling Configuration
&lt;/h3&gt;

&lt;p&gt;Application Insights enables &lt;strong&gt;adaptive sampling&lt;/strong&gt; by default, targeting 20 telemetry items per second per host. At low volume, you won't notice. At scale, sampling can silently discard traces, dependencies, and custom events before they reach your workspace.&lt;/p&gt;

&lt;p&gt;The recommended production configuration excludes &lt;code&gt;Request&lt;/code&gt; and &lt;code&gt;Exception&lt;/code&gt; from sampling, so you never lose function execution records or error details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"applicationInsights"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"samplingSettings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"isEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"maxTelemetryItemsPerSecond"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"excludedTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Request;Exception"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To check whether sampling is actively dropping data, run this KQL query. Any row where &lt;code&gt;TelemetrySavedPercentage&lt;/code&gt; is below 100 means that telemetry type is being sampled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;union traces, dependencies, requests
| where timestamp &amp;gt; ago(1d)
| summarize
    TelemetrySavedPercentage = round(100.0 / avg(itemCount), 1),
    TelemetryDroppedPercentage = round(100.0 - 100.0 / avg(itemCount), 1)
    by bin(timestamp, 1h), itemType
| where TelemetrySavedPercentage &amp;lt; 100
| order by timestamp desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;itemCount&lt;/code&gt; field on each telemetry item tells you how many similar items it represents. An &lt;code&gt;itemCount&lt;/code&gt; of 5 means Application Insights kept one item and estimated it represents five. If your &lt;code&gt;traces&lt;/code&gt; show 30% dropped, either raise &lt;code&gt;maxTelemetryItemsPerSecond&lt;/code&gt; or add &lt;code&gt;Trace&lt;/code&gt; to &lt;code&gt;excludedTypes&lt;/code&gt; for the categories that matter most to your debugging workflow. Watch your ingestion costs, though: excluding too many types from sampling at high volume can push you past your daily data cap quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading Traces and Metrics
&lt;/h2&gt;

&lt;p&gt;Once telemetry is flowing into Application Insights, you need to know where to look and what to ask. The portal gives you three entry points: Transaction Search for hunting specific executions, Log queries (KQL) for anything that requires aggregation or correlation, and the Application Map for a visual snapshot of your function app's dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transaction Search
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Transaction Search&lt;/strong&gt; is the fastest way to find what happened to a specific function execution. Open it from the left nav in your Application Insights resource, or use the shortcut from the Investigate section of the overview blade.&lt;/p&gt;

&lt;p&gt;The filters that matter most for Azure Functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operation name&lt;/strong&gt;: the function name as registered in the runtime (e.g., &lt;code&gt;ProcessOrderFunction&lt;/code&gt;). Filter here when you want all executions of a specific function in a time window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result code&lt;/strong&gt;: for HTTP triggers, this is the HTTP status code (&lt;code&gt;200&lt;/code&gt;, &lt;code&gt;500&lt;/code&gt;, etc.). For non-HTTP triggers (queue, timer, blob), &lt;code&gt;0&lt;/code&gt; means success and &lt;code&gt;1&lt;/code&gt; means failure. Combine with operation name to pull only failed runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time range&lt;/strong&gt;: narrow this first, before adding other filters. Application Insights searches can time out on broad time ranges at high volume.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click any result to open the &lt;strong&gt;End-to-end transaction view&lt;/strong&gt;. This is where distributed tracing pays off: you'll see the full execution timeline as a Gantt chart, with your function's request at the top and every outbound dependency (HTTP calls to payment APIs, SQL queries, Service Bus operations) shown as child spans with their durations. If a queue-triggered &lt;code&gt;ProcessOrderFunction&lt;/code&gt; call failed at 2am, this view tells you whether the failure was in your code, in a downstream HTTP call, or in a database query.&lt;/p&gt;

&lt;p&gt;One limitation: Transaction Search shows individual telemetry items, not aggregated data. If you want to know "how many orders failed between 1am and 3am and which customer IDs were affected", you need KQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  KQL Essentials for Azure Functions
&lt;/h3&gt;

&lt;p&gt;All four tables you'll use most (&lt;code&gt;requests&lt;/code&gt;, &lt;code&gt;dependencies&lt;/code&gt;, &lt;code&gt;traces&lt;/code&gt;, &lt;code&gt;exceptions&lt;/code&gt;) share an &lt;code&gt;operation_Id&lt;/code&gt; column. That ID is the distributed trace ID that ties every log line, dependency call, and exception back to the single function invocation that produced them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finding slow executions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;requests&lt;/code&gt; table records every function invocation. &lt;code&gt;duration&lt;/code&gt; is in milliseconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requests
| where timestamp &amp;gt; ago(24h)
| where name == "ProcessOrderFunction"
| where duration &amp;gt; 5000
| project timestamp, id, duration, resultCode, operation_Id
| order by duration desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you the slowest &lt;code&gt;ProcessOrderFunction&lt;/code&gt; executions in the last 24 hours. Swap &lt;code&gt;&amp;gt; 5000&lt;/code&gt; for whatever your SLA threshold is. The &lt;code&gt;operation_Id&lt;/code&gt; in each row is your entry point into the full trace for that execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tracing a single request end-to-end&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You have an &lt;code&gt;operation_Id&lt;/code&gt; from a failed execution (from Transaction Search, from an alert, or from a support ticket). This query reconstructs everything that happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let traceId = "abc123def456";
union requests, dependencies, traces, exceptions
| where operation_Id == traceId
| project timestamp, itemType, name, message, duration, success, resultCode, customDimensions
| order by timestamp asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;union&lt;/code&gt; across all four tables is deliberate. A single function execution produces rows in multiple tables: a &lt;code&gt;requests&lt;/code&gt; row for the invocation itself, &lt;code&gt;dependencies&lt;/code&gt; rows for every outbound call, &lt;code&gt;traces&lt;/code&gt; rows for your &lt;code&gt;ILogger&lt;/code&gt; calls, and an &lt;code&gt;exceptions&lt;/code&gt; row if something threw. The &lt;code&gt;itemType&lt;/code&gt; column tells you which table each row came from.&lt;/p&gt;

&lt;p&gt;If you set up &lt;code&gt;BeginScope&lt;/code&gt; with &lt;code&gt;OrderId&lt;/code&gt; and &lt;code&gt;CustomerId&lt;/code&gt; as described in the logging section, those values appear in &lt;code&gt;customDimensions&lt;/code&gt; on every trace row. You can also work backwards from a business ID when you don't have an &lt;code&gt;operation_Id&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;traces
| where timestamp &amp;gt; ago(24h)
| where customDimensions.OrderId == "ORD-20260330-1847"
| project operation_Id
| distinct operation_Id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take that &lt;code&gt;operation_Id&lt;/code&gt; and feed it into the union query above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Counting failures by function name over time&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requests
| where timestamp &amp;gt; ago(7d)
| where success == false
| summarize FailureCount = count() by bin(timestamp, 1h), name
| order by timestamp desc, FailureCount desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This surfaces which functions fail most, and whether failures cluster at specific times (a sign of a dependency being unhealthy during a maintenance window, or a batch job hitting a resource limit).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finding dependency bottlenecks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your function may be fast; a downstream service may not be.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dependencies
| where timestamp &amp;gt; ago(24h)
| where cloud_RoleName == "your-function-app-name"
| summarize
    CallCount = count(),
    P50 = percentile(duration, 50),
    P95 = percentile(duration, 95),
    P99 = percentile(duration, 99),
    FailureRate = round(100.0 * countif(success == false) / count(), 1)
    by target, type
| order by P95 desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;"your-function-app-name"&lt;/code&gt; with the value in your function app's Application Insights configuration (it defaults to the function app name). The &lt;code&gt;target&lt;/code&gt; column shows the external endpoint or database, and &lt;code&gt;type&lt;/code&gt; shows the dependency kind (&lt;code&gt;HTTP&lt;/code&gt;, &lt;code&gt;SQL&lt;/code&gt;, &lt;code&gt;Azure Service Bus&lt;/code&gt;, etc.). A high &lt;code&gt;P95&lt;/code&gt; with a low &lt;code&gt;FailureRate&lt;/code&gt; means the dependency is slow but not failing outright: the kind of problem that shows up as user-visible latency before it shows up as errors.&lt;/p&gt;

&lt;p&gt;One gotcha with KQL in the portal: queries run against a Log Analytics workspace, and there's a default query scope. If you open KQL from the Application Insights blade, you're automatically scoped to that resource's workspace. If you open it from a general Log Analytics workspace, you need to add &lt;code&gt;| where cloud_RoleName == "your-function-app-name"&lt;/code&gt; to every query, or you'll get results mixed across all resources in the workspace.&lt;/p&gt;

&lt;h3&gt;
  
  
  Application Map
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Application Map&lt;/strong&gt; (left nav, under Investigate) renders your function app as a node and every dependency it calls as connected nodes. Each connection shows call volume, average duration, and failure rate. Nodes turn yellow when failure rates exceed roughly 20-30% and red above 50% (the thresholds aren't configurable).&lt;/p&gt;

&lt;p&gt;For a &lt;code&gt;ProcessOrderFunction&lt;/code&gt; that calls a payment API and writes to SQL, you'd see three nodes: your function app in the centre, the payment API to one side, and the SQL database to the other. The lines between them show call counts and P95 latency. If the payment API node is yellow, that's your first place to look during an incident.&lt;/p&gt;

&lt;p&gt;The map is useful for a quick health check and for onboarding new team members, but it has limits. It aggregates across all functions in the app, so if you have ten functions and one is hammering a slow dependency, the map shows the aggregate. It also doesn't distinguish between functions calling the same dependency: if both &lt;code&gt;ProcessOrderFunction&lt;/code&gt; and &lt;code&gt;RefundOrderFunction&lt;/code&gt; call the same SQL database, the database shows one aggregated node. For function-level dependency analysis, go back to the KQL query above.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alerts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Alert rules&lt;/strong&gt; in Application Insights let you define a condition and trigger an action group (email, Teams webhook, PagerDuty, etc.) when the condition is met. You configure them under the Alerts section of your Application Insights resource.&lt;/p&gt;

&lt;p&gt;To create a failure rate alert for &lt;code&gt;ProcessOrderFunction&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select &lt;strong&gt;Create &amp;gt; Alert rule&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set the signal to &lt;strong&gt;Custom log search&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use this KQL as the condition:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requests
| where name == "ProcessOrderFunction"
| where timestamp &amp;gt; ago(5m)
| summarize
    Total = count(),
    Failed = countif(success == false)
| extend FailureRate = round(100.0 * Failed / Total, 1)
| where FailureRate &amp;gt; 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set evaluation frequency to every 1 minute, and the lookback window to 5 minutes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configure the threshold: alert when the query returns any rows (meaning &lt;code&gt;FailureRate &amp;gt; 5&lt;/code&gt; for that window).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Action groups&lt;/strong&gt; are the notification mechanism. One action group can send email, post to a Teams incoming webhook, and call an Azure Automation runbook simultaneously. Define your on-call action group once, then reuse it across all alert rules.&lt;/p&gt;

&lt;p&gt;A few practical notes on alert tuning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with a 5-minute window and a 5% threshold, then tighten after you've seen a few weeks of baseline data. Alerting on 1-minute windows at 1% failure rate on a low-volume function produces a lot of noise for transient errors.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;requests&lt;/code&gt; table has a 1-2 minute ingestion delay under normal conditions and up to 5 minutes during ingestion spikes. A 5-minute lookback window accounts for this. A 1-minute window can miss failures entirely if ingestion is delayed.&lt;/li&gt;
&lt;li&gt;For queue-triggered functions, complement failure rate alerts with a &lt;strong&gt;queue depth alert&lt;/strong&gt; on the source queue (configured through Azure Monitor metrics, not Application Insights). A growing queue combined with low invocation count means your function is failing at startup, before it even executes: a scenario that produces no &lt;code&gt;requests&lt;/code&gt; rows and won't trigger a failure rate alert.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Issues and Fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "My logs aren't showing up in Application Insights"
&lt;/h3&gt;

&lt;p&gt;It comes down to one of four things, and you can rule them out in order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check the connection string first.&lt;/strong&gt; Open your function app in the portal, go to Configuration, and confirm &lt;code&gt;APPLICATIONINSIGHTS_CONNECTION_STRING&lt;/code&gt; is set and points to the right resource. If it's missing or set to an instrumentation key only (the &lt;code&gt;InstrumentationKey=&amp;lt;guid&amp;gt;&lt;/code&gt; format without an &lt;code&gt;IngestionEndpoint&lt;/code&gt;), nothing reaches Application Insights at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check the worker log level config.&lt;/strong&gt; As covered in the two-pipeline gotcha above: &lt;code&gt;host.json&lt;/code&gt; controls the host process, but your &lt;code&gt;Program.cs&lt;/code&gt; or &lt;code&gt;appsettings.json&lt;/code&gt; controls the worker. If you haven't explicitly configured the worker's &lt;code&gt;ApplicationInsights&lt;/code&gt; log level, the SDK default applies: &lt;code&gt;Warning&lt;/code&gt; only. Add this to your &lt;code&gt;appsettings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ApplicationInsights"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or remove the default filter rule entirely in &lt;code&gt;Program.cs&lt;/code&gt;, as shown in the setup section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check sampling.&lt;/strong&gt; If requests appear but traces for specific functions don't, sampling may be discarding them. Run the KQL query from the sampling section to see which telemetry types are being dropped. Add &lt;code&gt;Trace&lt;/code&gt; to &lt;code&gt;excludedTypes&lt;/code&gt; in &lt;code&gt;host.json&lt;/code&gt; if you need full trace fidelity for a critical function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check the &lt;code&gt;Function&lt;/code&gt; log level in &lt;code&gt;host.json&lt;/code&gt;.&lt;/strong&gt; If &lt;code&gt;Function&lt;/code&gt; is set to &lt;code&gt;Warning&lt;/code&gt; or higher, &lt;code&gt;LogInformation&lt;/code&gt; calls from your function code never leave the host. Set it to &lt;code&gt;Information&lt;/code&gt; to restore them.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Dependencies are missing from the Application Map"
&lt;/h3&gt;

&lt;p&gt;When your Application Map shows your function app as an isolated node with no outbound edges, check for a missing &lt;code&gt;ConfigureFunctionsApplicationInsights()&lt;/code&gt; call in &lt;code&gt;Program.cs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AddApplicationInsightsTelemetryWorkerService()&lt;/code&gt; registers the SDK. &lt;code&gt;ConfigureFunctionsApplicationInsights()&lt;/code&gt; is what connects the Functions runtime's &lt;code&gt;ActivitySource&lt;/code&gt; to that SDK so outbound HTTP, SQL, and Azure SDK calls produce dependency telemetry with the correct operation IDs. Without it, dependencies are either not tracked at all or tracked with broken correlation (they appear in the &lt;code&gt;dependencies&lt;/code&gt; table but don't link back to the parent request).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddApplicationInsightsTelemetryWorkerService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsApplicationInsights&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Required for dependency tracking&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both calls are present and you're still missing HTTP dependencies: check how &lt;code&gt;HttpClient&lt;/code&gt; is registered. The Application Insights SDK instruments &lt;code&gt;HttpClient&lt;/code&gt; via &lt;code&gt;IHttpClientFactory&lt;/code&gt;. If you're creating &lt;code&gt;HttpClient&lt;/code&gt; instances with &lt;code&gt;new HttpClient()&lt;/code&gt; directly instead of injecting an &lt;code&gt;IHttpClientFactory&lt;/code&gt;-managed instance, those calls bypass the instrumentation entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Not tracked&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Tracked (inject IHttpClientFactory via primary constructor)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IHttpClientFactory&lt;/span&gt; &lt;span class="n"&gt;httpClientFactory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpClientFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register it in &lt;code&gt;Program.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  "I see duplicate telemetry for every request"
&lt;/h3&gt;

&lt;p&gt;In the isolated worker model, both the host process and the worker process can emit telemetry for the same function invocation. When both are sending to the same Application Insights resource, you get duplicate &lt;code&gt;requests&lt;/code&gt; entries, inflated counts, and misleading failure rates.&lt;/p&gt;

&lt;p&gt;This is controlled by the &lt;code&gt;telemetryMode&lt;/code&gt; setting at the &lt;strong&gt;root level&lt;/strong&gt; of &lt;code&gt;host.json&lt;/code&gt; (not inside &lt;code&gt;logging&lt;/code&gt;). The default is &lt;code&gt;default&lt;/code&gt;, which allows both sides to emit. Setting it to &lt;code&gt;OpenTelemetry&lt;/code&gt; resolves the duplication, but note that when you do, the &lt;code&gt;logging.applicationInsights&lt;/code&gt; section of &lt;code&gt;host.json&lt;/code&gt; no longer applies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"telemetryMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OpenTelemetry"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, suppress host-side request telemetry while keeping your worker-side telemetry by raising &lt;code&gt;Host.Results&lt;/code&gt; above &lt;code&gt;Information&lt;/code&gt; in &lt;code&gt;host.json&lt;/code&gt;'s &lt;code&gt;logLevel&lt;/code&gt; section. The tradeoff: this also removes successful execution records from the portal's Function Monitor tab. Use &lt;code&gt;telemetryMode&lt;/code&gt; when you want clean deduplication without losing host-side visibility.&lt;/p&gt;

&lt;p&gt;To confirm duplication before changing anything, run this in KQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requests
| where timestamp &amp;gt; ago(1h)
| summarize count() by operation_Id
| where count_ &amp;gt; 1
| order by count_ desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any &lt;code&gt;operation_Id&lt;/code&gt; appearing more than once is a duplicated invocation.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Cold start latency spikes in my metrics"
&lt;/h3&gt;

&lt;p&gt;Cold starts produce latency spikes that look identical to slow execution in your metrics. Before investigating application code, confirm whether a spike is a cold start or an actual regression.&lt;/p&gt;

&lt;p&gt;A cold start request carries a specific pattern: high latency on the first invocation from a given instance, with subsequent requests from the same instance running at normal duration. The &lt;code&gt;cloud_RoleInstance&lt;/code&gt; dimension on each request record identifies the instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requests
| where timestamp &amp;gt; ago(24h)
| where name == "ProcessOrderFunction"
| summarize
    first_request = min(timestamp),
    p50 = percentile(duration, 50),
    p99 = percentile(duration, 99),
    request_count = count()
    by cloud_RoleInstance
| extend is_cold_start_instance = (request_count &amp;lt;= 3)
| order by first_request desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instances where &lt;code&gt;request_count&lt;/code&gt; is 1 or 2 are almost certainly fresh scale-out instances, and their durations are not representative of your steady-state performance. Filter them out when computing your SLA metrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requests
| where timestamp &amp;gt; ago(24h)
| where name == "ProcessOrderFunction"
| join kind=inner (
    requests
    | summarize request_count = count() by cloud_RoleInstance
    | where request_count &amp;gt; 5
) on cloud_RoleInstance
| summarize p50 = percentile(duration, 50), p99 = percentile(duration, 99)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the spike appears in warm instances, you have a real slowdown. If it's limited to fresh instances appearing after a scale-out event, it's cold start behavior. The two require different responses: cold starts call for pre-warming strategies or Consumption to Premium plan migration; actual slowdowns point to profiling.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Alerts fire but I can't find the failing requests"
&lt;/h3&gt;

&lt;p&gt;You set up an alert on exception count, it fires, you open the Failures blade, and the requests that caused the exceptions are gone. Sampling is discarding the evidence.&lt;/p&gt;

&lt;p&gt;By default, &lt;code&gt;Exception&lt;/code&gt; telemetry is sampled alongside everything else. When the SDK keeps one exception and estimates it represents five, the other four are discarded permanently. Your alert fires because the metric aggregation (which runs before sampling discards anything) saw all five. Your query returns only the one that survived.&lt;/p&gt;

&lt;p&gt;The fix is to exclude &lt;code&gt;Exception&lt;/code&gt; from sampling in &lt;code&gt;host.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"applicationInsights"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"samplingSettings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"isEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"maxTelemetryItemsPerSecond"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"excludedTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Request;Exception"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding &lt;code&gt;Request&lt;/code&gt; to &lt;code&gt;excludedTypes&lt;/code&gt; ensures the parent request record is also always kept, so you can correlate the exception back to its invocation through the &lt;code&gt;operation_Id&lt;/code&gt;. Without both, you may find the exception but not the request that caused it.&lt;/p&gt;

&lt;p&gt;If the alert is on a custom metric rather than exceptions, check whether &lt;code&gt;customMetrics&lt;/code&gt; is being sampled. Custom metrics emitted through &lt;code&gt;TelemetryClient.GetMetric()&lt;/code&gt; are not affected by sampling (they're pre-aggregated in the SDK before sending). Custom events emitted with &lt;code&gt;TelemetryClient.TrackEvent()&lt;/code&gt; are sampled, and alerts based on custom event counts can suffer the same problem. Add &lt;code&gt;Event&lt;/code&gt; to &lt;code&gt;excludedTypes&lt;/code&gt; if that's your signal source.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenTelemetry Alternative
&lt;/h2&gt;

&lt;p&gt;The classic Application Insights SDK works well if your entire stack lives in Azure. But if you need to send telemetry to Grafana, Datadog, Jaeger, or any other backend alongside (or instead of) Azure Monitor, you're duplicating instrumentation code for each target. &lt;strong&gt;OpenTelemetry&lt;/strong&gt; solves this at the protocol level: one set of instrumentation, one exporter interface, multiple backends.&lt;/p&gt;

&lt;p&gt;OpenTelemetry is a CNCF project that defines a vendor-neutral API and wire format (OTLP) for traces, metrics, and logs. The same instrumentation code that sends data to Application Insights can also send to Zipkin or Collector pipelines with a config change, not a code rewrite.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Setup
&lt;/h3&gt;

&lt;p&gt;Microsoft publishes the &lt;strong&gt;&lt;code&gt;Microsoft.Azure.Functions.Worker.OpenTelemetry&lt;/code&gt;&lt;/strong&gt; package for this purpose, paired with the Azure Monitor exporter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Microsoft.Azure.Functions.Worker.OpenTelemetry
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package Azure.Monitor.OpenTelemetry.Exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, enable OpenTelemetry output from the Functions host by adding &lt;code&gt;"telemetryMode": "OpenTelemetry"&lt;/code&gt; at the root of your &lt;code&gt;host.json&lt;/code&gt; (the same setting described in the duplicate telemetry section). Then the &lt;code&gt;Program.cs&lt;/code&gt; registration replaces the classic SDK calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Azure.Monitor.OpenTelemetry.Exporter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker.Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker.OpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Hosting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseFunctionsWorkerDefaults&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAzureMonitorExporter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// reads APPLICATIONINSIGHTS_CONNECTION_STRING automatically&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UseFunctionsWorkerDefaults()&lt;/code&gt; hooks into the Functions runtime's &lt;code&gt;ActivitySource&lt;/code&gt; for proper distributed trace correlation (the OpenTelemetry equivalent of &lt;code&gt;ConfigureFunctionsApplicationInsights()&lt;/code&gt; from the classic SDK). Without it, dependency telemetry won't correlate back to the parent function invocation. See the full setup in &lt;a href="https://github.com/MO2k4/azure-functions-samples/blob/main/EventHubDemo/Program.cs" rel="noopener noreferrer"&gt;EventHubDemo/Program.cs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The connection string is still required; &lt;code&gt;UseAzureMonitorExporter()&lt;/code&gt; reads &lt;code&gt;APPLICATIONINSIGHTS_CONNECTION_STRING&lt;/code&gt; from the environment the same way the classic SDK does. If you also want to export to a second backend (requires the additional &lt;code&gt;OpenTelemetry.Exporter.OpenTelemetryProtocol&lt;/code&gt; package), register it separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseFunctionsWorkerDefaults&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAzureMonitorExporter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithTracing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tracing&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tracing&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOtlpExporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;otlp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;otlp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:4317"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UseAzureMonitorExporter()&lt;/code&gt; is a cross-cutting registration that configures all signals at once. Chaining signal-specific exporters like &lt;code&gt;AddOtlpExporter&lt;/code&gt; after it in the same builder can throw a &lt;code&gt;NotSupportedException&lt;/code&gt;. Separate &lt;code&gt;AddOpenTelemetry()&lt;/code&gt; calls avoid the conflict.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Gain
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;W3C Trace Context&lt;/strong&gt; is the default propagation format, which means your distributed traces correlate correctly with other OpenTelemetry-instrumented services regardless of what backend they report to. With the classic SDK you get this too, but only within the Application Insights ecosystem; outside it, the format diverges.&lt;/p&gt;

&lt;p&gt;You also get &lt;strong&gt;multi-backend export&lt;/strong&gt;: Azure Monitor for your ops team, a Grafana stack for your platform team, and a local Collector for local debugging, all from the same process. And if you ever migrate off Azure Monitor entirely, you replace one exporter registration, not every &lt;code&gt;TelemetryClient&lt;/code&gt; call in your codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Lose Today
&lt;/h3&gt;

&lt;p&gt;The OpenTelemetry path is not at feature parity with the classic SDK for Functions specifically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live Metrics&lt;/strong&gt; (the real-time stream at &lt;code&gt;monitor.azure.com&lt;/code&gt;) does not work with the distro. It relies on a proprietary push mechanism in the classic SDK that has no OpenTelemetry equivalent yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot Debugger&lt;/strong&gt; is unavailable. It's a classic SDK feature with no OTLP counterpart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-collection gaps&lt;/strong&gt;: some dependency types that the classic SDK instruments automatically (certain Azure SDK operations, Service Bus settlement calls) may not be captured out of the box, depending on which OpenTelemetry instrumentation libraries you've added. You may need to add &lt;code&gt;AddAzureClientsInstrumentation()&lt;/code&gt; or equivalent packages explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation&lt;/strong&gt;: the distro's documentation for Functions scenarios specifically is thin. Most samples target ASP.NET Core web apps; you'll spend time adapting them and testing whether auto-collection works for your trigger types.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to Choose Which
&lt;/h3&gt;

&lt;p&gt;Use the &lt;strong&gt;classic SDK&lt;/strong&gt; if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your entire workload runs on Azure and you have no multi-vendor requirements&lt;/li&gt;
&lt;li&gt;you need Live Metrics or Snapshot Debugger&lt;/li&gt;
&lt;li&gt;you want the richest out-of-the-box experience with the Application Insights portal today&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;strong&gt;OpenTelemetry&lt;/strong&gt; if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you're sending telemetry to multiple backends, or planning to&lt;/li&gt;
&lt;li&gt;the rest of your services are already OpenTelemetry-instrumented and you need consistent trace propagation across the board&lt;/li&gt;
&lt;li&gt;you're building something that might not always live in Azure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're greenfield on a purely Azure stack, the classic SDK is less configuration for the same result right now. If you're instrumenting a heterogeneous system or building for portability, OpenTelemetry's overhead is worth it; you pay once at setup and gain the flexibility when requirements change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This is Part 9 and the final article in the core series. Over nine weeks, this series went from "what is serverless" to querying production telemetry in KQL. If you followed along and built something, you now have a function app with HTTP and queue triggers, proper configuration with Key Vault, a CI/CD pipeline through GitHub Actions, and Application Insights wired up for structured logging, distributed tracing, and alerting. That covers the full lifecycle: build, test, deploy, monitor.&lt;/p&gt;

&lt;p&gt;The companion repository at &lt;a href="https://github.com/MO2k4/azure-functions-samples" rel="noopener noreferrer"&gt;azure-functions-samples&lt;/a&gt; has working code for every article in the series. Clone it, break things, wire up your own alerts.&lt;/p&gt;

&lt;p&gt;Next week is a bonus article outside the core series: production cost realities on the Consumption plan, and the signals that tell you it's time to move to Flex Consumption or Premium. If you've ever wondered why your monthly bill looked nothing like the pricing calculator, that one is for you.&lt;/p&gt;

&lt;p&gt;When you first wired up monitoring on a production function app, which alert did you set up first: failure rate or latency?&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 9: Azure Functions Observability: From Blind Spots to Production Clarity (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>dotnet</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Deploying to Azure: CI/CD with GitHub Actions</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 27 Mar 2026 06:36:06 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m</link>
      <guid>https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m</guid>
      <description>&lt;h2&gt;
  
  
  Introduction: from local to production
&lt;/h2&gt;

&lt;p&gt;Local tooling hides four things you have to own in production: packaging, authentication, configuration injection, and rollback. &lt;code&gt;func start&lt;/code&gt; handles all of them silently; a CI/CD pipeline does not, and the decisions you make about each one compound quickly.&lt;/p&gt;

&lt;p&gt;The gap is easy to miss. Your local environment reads from &lt;code&gt;local.settings.json&lt;/code&gt;, authenticates with your personal identity, and recovers from bad deploys by letting you just restart. Azure does none of that for you. You need a packaging step, a way to authenticate from a pipeline without storing secrets, a strategy for injecting environment-specific configuration, and some mechanism for rolling back when a deploy breaks something.&lt;/p&gt;

&lt;p&gt;This article covers two stages of that journey. First, manual deployment using the Azure CLI and the Functions Core Tools: useful for quick validation and understanding what the automated pipeline will do under the hood. Then a GitHub Actions workflow with two jobs, OIDC authentication (no stored credentials in your repository), deployment slots for zero-downtime releases, and configuration management that keeps secrets out of your pipeline definition entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual deployment options
&lt;/h2&gt;

&lt;p&gt;Before wiring up a full CI/CD pipeline, understand what actually happens when code reaches Azure. Manual deployment gives you that visibility, and it remains useful long after you've automated everything: for one-off hotfixes, for validating a packaging issue, or for deploying to a scratch environment without spinning up a workflow run.&lt;/p&gt;

&lt;h3&gt;
  
  
  func azure functionapp publish
&lt;/h3&gt;

&lt;p&gt;The Core Tools command is the closest thing to a one-stop deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;func azure functionapp publish &amp;lt;APP_NAME&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, it runs &lt;code&gt;dotnet build --output bin/publish&lt;/code&gt;, creates a &lt;code&gt;.zip&lt;/code&gt; archive (filtered by your &lt;code&gt;.funcignore&lt;/code&gt;), uploads the archive via the Kudu ZipDeploy API (or One Deploy for Flex Consumption plans), and then syncs triggers and restarts the host. By default it also sets &lt;code&gt;WEBSITE_RUN_FROM_PACKAGE=1&lt;/code&gt; on the app, covered in the next subsection.&lt;/p&gt;

&lt;p&gt;Flags you'll reach for regularly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Skip the local build — useful when you've already built in CI&lt;/span&gt;
func azure functionapp publish &amp;lt;APP_NAME&amp;gt; &lt;span class="nt"&gt;--no-build&lt;/span&gt;

&lt;span class="c"&gt;# Deploy to a staging slot instead of production&lt;/span&gt;
func azure functionapp publish &amp;lt;APP_NAME&amp;gt; &lt;span class="nt"&gt;--slot&lt;/span&gt; staging

&lt;span class="c"&gt;# Push local.settings.json values to app settings (prompts for confirmation)&lt;/span&gt;
func azure functionapp publish &amp;lt;APP_NAME&amp;gt; &lt;span class="nt"&gt;--publish-local-settings&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt;

&lt;span class="c"&gt;# Verify what files will be included before committing to a deploy&lt;/span&gt;
func azure functionapp publish &amp;lt;APP_NAME&amp;gt; &lt;span class="nt"&gt;--list-included-files&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;--list-included-files&lt;/code&gt; at least once per project. If your archive includes &lt;code&gt;bin/&lt;/code&gt; debug artifacts, test assemblies, or secrets you meant to &lt;code&gt;.funcignore&lt;/code&gt;, you want to catch that before it's sitting on a production host.&lt;/p&gt;

&lt;p&gt;A minimal &lt;code&gt;.funcignore&lt;/code&gt; for a .NET project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*.csproj
*.sln
.git/
.vscode/
local.settings.json
test/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;local.settings.json&lt;/code&gt; is the most important exclusion: it often contains connection strings and keys meant for local development only.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure CLI: two commands, two APIs
&lt;/h3&gt;

&lt;p&gt;The Azure CLI gives you two distinct options, and picking the wrong one for your plan type will fail silently or throw a confusing error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Kudu ZipDeploy — works for Consumption, Premium, and Dedicated plans&lt;/span&gt;
az functionapp deployment &lt;span class="nb"&gt;source &lt;/span&gt;config-zip &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-g&lt;/span&gt; &amp;lt;RESOURCE_GROUP&amp;gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &amp;lt;APP_NAME&amp;gt; &lt;span class="nt"&gt;--src&lt;/span&gt; ./publish.zip

&lt;span class="c"&gt;# One Deploy API — required for Flex Consumption, also valid elsewhere&lt;/span&gt;
az functionapp deploy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-g&lt;/span&gt; &amp;lt;RESOURCE_GROUP&amp;gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &amp;lt;APP_NAME&amp;gt; &lt;span class="nt"&gt;--src-path&lt;/span&gt; ./publish.zip &lt;span class="nt"&gt;--type&lt;/span&gt; zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The older &lt;code&gt;config-zip&lt;/code&gt; command talks directly to Kudu and does no building; you're responsible for providing a publish-ready zip. It does not support &lt;strong&gt;Flex Consumption&lt;/strong&gt;, the newer serverless plan that bypasses Kudu entirely. If you're on Flex Consumption, &lt;code&gt;az functionapp deploy&lt;/code&gt; is the only CLI path that works. It also gives you &lt;code&gt;--clean&lt;/code&gt; to remove files not in the archive and &lt;code&gt;--async&lt;/code&gt; to return immediately without polling for completion.&lt;/p&gt;

&lt;p&gt;A rule of thumb: if you're writing a deploy script that needs to work across plan types, use &lt;code&gt;az functionapp deploy&lt;/code&gt;. If you're on a legacy plan and &lt;code&gt;config-zip&lt;/code&gt; already exists in your runbooks, it's fine to leave it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run-From-Package and why it matters
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;WEBSITE_RUN_FROM_PACKAGE=1&lt;/code&gt; is set, Azure mounts your zip archive as a read-only filesystem at &lt;code&gt;wwwroot&lt;/code&gt; rather than extracting files into it. This is the default behavior when you publish with Core Tools, and it has real production benefits: deployment is &lt;strong&gt;atomic&lt;/strong&gt; (the old package stays mounted until the new one is ready), file-copy locking errors disappear, and cold start times improve because the runtime reads directly from the zip.&lt;/p&gt;

&lt;p&gt;The constraints: &lt;code&gt;wwwroot&lt;/code&gt; becomes read-only (portal-based editing no longer works), the archive has a 1 GB limit, and you should not set this value on Flex Consumption plans, which manage packages differently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which method to use
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7lw0jrcxz5vedkpmv3s6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7lw0jrcxz5vedkpmv3s6.png" alt="Which method to use: func publish for development, config-zip for legacy scripts, az functionapp deploy for all plans" width="800" height="206"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For anything beyond a one-off fix or an afternoon prototype, these manual commands are the foundation you'll extract into a pipeline. Knowing what each one does makes the GitHub Actions steps in the next section easier to reason about when something goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Actions workflow setup
&lt;/h2&gt;

&lt;p&gt;The pieces fit together like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F22buyjo6967rdfy4zdab.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F22buyjo6967rdfy4zdab.png" alt="Deployment pipeline: Push to main, Build Job, Deploy Job with OIDC Login, Deploy to Staging Slot, Swap to Production" width="800" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The build job produces a single artifact. The deploy job authenticates via OIDC, pushes to a staging slot, and swaps it into production.&lt;/p&gt;

&lt;p&gt;The complete workflow is below. Read through it first; the walkthrough after explains the decisions behind each piece.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Azure Functions (.NET 10)&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-dotnet@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;dotnet-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;10.0.x'&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet restore --locked-mode&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;dotnet publish src/MyFunctionApp&lt;/span&gt;
          &lt;span class="s"&gt;--configuration Release&lt;/span&gt;
          &lt;span class="s"&gt;--output ./output&lt;/span&gt;
          &lt;span class="s"&gt;--runtime linux-x64&lt;/span&gt;
          &lt;span class="s"&gt;--self-contained true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;function-app&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./output&lt;/span&gt;
          &lt;span class="na"&gt;retention-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/download-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;function-app&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./output&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_CLIENT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;tenant-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_TENANT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;subscription-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_SUBSCRIPTION_ID }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Azure/functions-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;app-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.FUNCTION_APP_NAME }}&lt;/span&gt;
          &lt;span class="na"&gt;slot-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;staging&lt;/span&gt;
          &lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./output&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Swap staging to production&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/cli@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;inlineScript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;az functionapp deployment slot swap \&lt;/span&gt;
              &lt;span class="s"&gt;--name ${{ vars.FUNCTION_APP_NAME }} \&lt;/span&gt;
              &lt;span class="s"&gt;--resource-group ${{ vars.RESOURCE_GROUP }} \&lt;/span&gt;
              &lt;span class="s"&gt;--slot staging \&lt;/span&gt;
              &lt;span class="s"&gt;--target-slot production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same function app from Parts 1 through 7. The complete source is in the &lt;a href="https://github.com/MO2k4/azure-functions-samples" rel="noopener noreferrer"&gt;azure-functions-samples&lt;/a&gt; repository. Every push to &lt;code&gt;main&lt;/code&gt; builds it, deploys to a staging slot, and swaps to production. No secrets stored, no manual steps, and a rollback is one swap away.&lt;/p&gt;

&lt;p&gt;If your plan doesn't support slots (Consumption with only one slot available, or Flex Consumption), remove the &lt;code&gt;slot-name&lt;/code&gt; parameter and the swap step. The &lt;code&gt;functions-action&lt;/code&gt; will deploy directly to production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why two jobs instead of one
&lt;/h3&gt;

&lt;p&gt;The split between &lt;code&gt;build&lt;/code&gt; and &lt;code&gt;deploy&lt;/code&gt; exists for two reasons.&lt;/p&gt;

&lt;p&gt;First, the artifact produced by &lt;code&gt;build&lt;/code&gt; is reusable. If you add a staging environment later, the deploy job can run twice against the same artifact without rebuilding. Build once, deploy to as many environments as you need.&lt;/p&gt;

&lt;p&gt;Second, the &lt;strong&gt;&lt;code&gt;id-token: write&lt;/code&gt; permission&lt;/strong&gt; required for OIDC authentication (covered in the next section) is scoped to the &lt;code&gt;deploy&lt;/code&gt; job only. If you set it at the workflow level, every job gets that elevated permission. Keeping it on the deploy job limits the blast radius if something goes wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  The build job
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;actions/checkout@v4&lt;/code&gt; pulls your code. &lt;code&gt;actions/setup-dotnet@v4&lt;/code&gt; installs the SDK, and the &lt;code&gt;cache: true&lt;/code&gt; option tells it to cache the NuGet package cache between runs.&lt;/p&gt;

&lt;p&gt;That cache only works if your project has a &lt;strong&gt;lock file&lt;/strong&gt;. Add this to your &lt;code&gt;.csproj&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;RestorePackagesWithLockFile&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/RestorePackagesWithLockFile&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then commit the generated &lt;code&gt;packages.lock.json&lt;/code&gt;. Without it, &lt;code&gt;cache: true&lt;/code&gt; has nothing to hash (so every run misses the cache), and &lt;code&gt;--locked-mode&lt;/code&gt; silently regenerates a new lock file instead of validating against a committed one. With both in place, clean builds skip the network entirely for packages that haven't changed.&lt;/p&gt;

&lt;p&gt;The publish step is where .NET 10 requires extra care:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet publish src/MyFunctionApp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--configuration&lt;/span&gt; Release &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; ./output &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--runtime&lt;/span&gt; linux-x64 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--self-contained&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--self-contained true&lt;/code&gt; is required for .NET 10. The Azure Functions v4 host runs on .NET 8. If you publish a &lt;strong&gt;framework-dependent&lt;/strong&gt; app targeting .NET 10, the host cannot find the .NET 10 runtime and the deployment fails with exit code 150 (&lt;code&gt;0x96&lt;/code&gt;). A self-contained publish bundles the runtime with your app, so the host's .NET version becomes irrelevant.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;actions/upload-artifact@v4&lt;/code&gt; takes the &lt;code&gt;./output&lt;/code&gt; folder and makes it available to downstream jobs. The &lt;code&gt;name&lt;/code&gt; value (&lt;code&gt;function-app&lt;/code&gt;) is how the deploy job will refer to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The deploy job
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;needs: build&lt;/code&gt; means this job waits for the build to succeed before starting. &lt;code&gt;environment: production&lt;/code&gt; ties the job to a GitHub environment, which lets you add required reviewers or protection rules before any deployment proceeds.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;actions/download-artifact@v4&lt;/code&gt; retrieves the artifact by the same name used during upload and places it in &lt;code&gt;./output&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;azure/login@v2&lt;/code&gt; handles authentication using OIDC; the specifics of how to configure this are in the next section. This step must come before &lt;code&gt;functions-action&lt;/code&gt;, and the three secrets (&lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt;, &lt;code&gt;AZURE_TENANT_ID&lt;/code&gt;, &lt;code&gt;AZURE_SUBSCRIPTION_ID&lt;/code&gt;) must be set in your repository or environment settings.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Azure/functions-action@v1&lt;/code&gt; does the actual deployment. Two parameters are required: &lt;code&gt;app-name&lt;/code&gt; (the name of your Function App in Azure) and &lt;code&gt;package&lt;/code&gt; (the path to your artifact). An optional &lt;code&gt;slot-name&lt;/code&gt; parameter targets a deployment slot if you are using them.&lt;/p&gt;

&lt;p&gt;The deployment method the action uses depends on your hosting plan. Flex Consumption plans use &lt;strong&gt;One Deploy&lt;/strong&gt;; all other plans use &lt;strong&gt;Zip Deploy&lt;/strong&gt;. The action picks this automatically based on your app's plan type, so you do not need to configure it explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  OIDC authentication (no stored secrets)
&lt;/h2&gt;

&lt;p&gt;The workflow above uses three secrets: &lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt;, &lt;code&gt;AZURE_TENANT_ID&lt;/code&gt;, and &lt;code&gt;AZURE_SUBSCRIPTION_ID&lt;/code&gt;. None of them are actual credentials. That's the point of OIDC.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not publish profiles or service principal secrets?
&lt;/h3&gt;

&lt;p&gt;Publish profiles are XML files containing deployment credentials baked into the Function App. They work, but they create problems at scale: they can't be scoped to a branch or environment, they don't expire on a schedule, and if one leaks, anyone with the file can deploy to your app until you manually reset it.&lt;/p&gt;

&lt;p&gt;Service principal secrets are better (they support expiration and RBAC scoping), but you still have a secret stored in GitHub that needs rotating every 6-24 months. Miss a rotation and your pipeline breaks silently on the next deploy.&lt;/p&gt;

&lt;p&gt;OIDC eliminates stored credentials entirely. GitHub mints a short-lived token for each workflow run, Azure validates that token against a federated credential you configure once, and nothing secret ever sits in your repository settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Your workflow requests an OIDC token from GitHub's token service&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;azure/login&lt;/code&gt; action sends that token to Microsoft Entra ID&lt;/li&gt;
&lt;li&gt;Entra validates the token's issuer (&lt;code&gt;token.actions.githubusercontent.com&lt;/code&gt;), audience, and subject claim (which encodes the repo, branch, and environment)&lt;/li&gt;
&lt;li&gt;If the claims match your federated credential configuration, Entra issues an Azure access token&lt;/li&gt;
&lt;li&gt;The access token is used for the deployment, then expires&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The subject claim is what makes this granular. You can restrict a credential to only work from a specific environment (&lt;code&gt;repo:your-org/your-repo:environment:production&lt;/code&gt;), a specific branch, or even pull requests. A token minted from a feature branch won't match a credential scoped to the &lt;code&gt;production&lt;/code&gt; environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup steps
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Create an Entra app registration with a service principal:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az ad app create &lt;span class="nt"&gt;--display-name&lt;/span&gt; &lt;span class="s2"&gt;"github-deploy-my-func-app"&lt;/span&gt;
az ad sp create &lt;span class="nt"&gt;--id&lt;/span&gt; &amp;lt;APP_ID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Assign the &lt;code&gt;Website Contributor&lt;/code&gt; role&lt;/strong&gt; scoped to the resource group containing your Function App:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az role assignment create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assignee&lt;/span&gt; &amp;lt;APP_ID&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt; &lt;span class="s2"&gt;"Website Contributor"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scope&lt;/span&gt; /subscriptions/&amp;lt;SUB_ID&amp;gt;/resourceGroups/&amp;lt;RG_NAME&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Website Contributor&lt;/code&gt; is enough for deploying code. &lt;code&gt;Contributor&lt;/code&gt; works too but grants more access than the pipeline needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Configure a federated identity credential:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"github-actions-production"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://token.actions.githubusercontent.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"repo:your-org/your-repo:environment:production"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"audiences"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"api://AzureADTokenExchange"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az ad app federated-credential create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--id&lt;/span&gt; &amp;lt;APP_ID&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameters&lt;/span&gt; @credential.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;subject&lt;/code&gt; field must match exactly. If your deploy job uses &lt;code&gt;environment: production&lt;/code&gt;, the subject must end with &lt;code&gt;:environment:production&lt;/code&gt;. If you deploy from a branch without an environment, use &lt;code&gt;:ref:refs/heads/main&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Store the IDs as GitHub environment secrets:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go to your repository Settings &amp;gt; Environments &amp;gt; production &amp;gt; Environment secrets, and add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt;: the Application (client) ID from your app registration&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AZURE_TENANT_ID&lt;/code&gt;: your Entra tenant ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AZURE_SUBSCRIPTION_ID&lt;/code&gt;: the subscription containing your Function App&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are identifiers, not credentials. Even if someone reads them, they can't authenticate without a valid OIDC token from your specific repository and environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow permissions
&lt;/h3&gt;

&lt;p&gt;The deploy job needs &lt;code&gt;id-token: write&lt;/code&gt; to mint the OIDC token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set this on the deploy job only, not at the workflow level. The build job doesn't need token-minting permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  One gotcha
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Azure/functions-action&lt;/code&gt; supports two authentication methods: &lt;code&gt;publish-profile&lt;/code&gt; and the &lt;code&gt;azure/login&lt;/code&gt; action. They are &lt;strong&gt;mutually exclusive&lt;/strong&gt;. If you pass a &lt;code&gt;publish-profile&lt;/code&gt; parameter while also using &lt;code&gt;azure/login&lt;/code&gt;, the action uses the publish profile and ignores your OIDC session. Remove the &lt;code&gt;publish-profile&lt;/code&gt; parameter entirely when switching to OIDC.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment slots and zero-downtime releases
&lt;/h2&gt;

&lt;p&gt;Deploying directly to production means every release has a moment where either the old code or the new code is partially running. Deployment slots give you a staging URL to validate before any production traffic sees the new version, and an instant rollback if something goes wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  What each plan supports
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcae24fmuypp8l6htr2vs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcae24fmuypp8l6htr2vs.png" alt="Deployment slots by plan: Consumption 2, Premium 3, Dedicated 1-20, Flex Consumption not supported" width="466" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're on Flex Consumption, skip to the rolling updates section below.&lt;/p&gt;

&lt;h3&gt;
  
  
  The blue-green pattern
&lt;/h3&gt;

&lt;p&gt;Deploy to a staging slot, verify it works, then swap staging into production.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deploy to staging&lt;/strong&gt;: your CI/CD pipeline targets the &lt;code&gt;staging&lt;/code&gt; slot instead of production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate&lt;/strong&gt;: hit the staging URL (&lt;code&gt;your-func-app-staging.azurewebsites.net&lt;/code&gt;) with smoke tests or manual checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap&lt;/strong&gt;: Azure switches the routing so staging serves production traffic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback if needed&lt;/strong&gt;: swap again to revert (the old production code is now in the staging slot)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The swap itself takes seconds. Your users see either the old version or the new version, never a half-deployed state.&lt;/p&gt;

&lt;h3&gt;
  
  
  What swaps and what stays
&lt;/h3&gt;

&lt;p&gt;This trips people up. During a swap, &lt;strong&gt;code and most settings travel together&lt;/strong&gt; from staging to production. But some things are pinned to the slot:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Travels with code (gets swapped):&lt;/strong&gt; general app settings (unless marked sticky), connection strings (unless marked sticky), handler mappings, public certificates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stays with the slot:&lt;/strong&gt; publishing endpoints, custom domains, TLS/SSL certificates, scale settings, IP restrictions, Always On, &lt;code&gt;FUNCTIONS_EXTENSION_VERSION&lt;/code&gt; (sticky by default).&lt;/p&gt;

&lt;h3&gt;
  
  
  Sticky settings that cause problems
&lt;/h3&gt;

&lt;p&gt;Two settings deserve special attention:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;FUNCTIONS_EXTENSION_VERSION&lt;/code&gt;&lt;/strong&gt; is sticky by default. If your staging slot runs &lt;code&gt;~4&lt;/code&gt; and production also runs &lt;code&gt;~4&lt;/code&gt;, this is invisible. But if you ever need to change the version, the stickiness means the setting won't swap with the code. To make it travel with the swap, set &lt;code&gt;WEBSITE_OVERRIDE_STICKY_EXTENSION_VERSIONS=0&lt;/code&gt; on &lt;strong&gt;all&lt;/strong&gt; slots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;WEBSITE_CONTENTSHARE&lt;/code&gt;&lt;/strong&gt; is auto-generated per slot and should never be set manually. Each slot needs its own content share to avoid file locking conflicts. If you see deployment failures mentioning "cannot access file," check whether slots are sharing this value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy-to-slot and swap in GitHub Actions
&lt;/h3&gt;

&lt;p&gt;Add &lt;code&gt;slot-name&lt;/code&gt; to the deploy step, then swap using the Azure CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Azure/functions-action@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-func-app'&lt;/span&gt;
    &lt;span class="na"&gt;slot-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;staging&lt;/span&gt;
    &lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./output&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Swap staging to production&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/cli@v2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inlineScript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;az functionapp deployment slot swap \&lt;/span&gt;
        &lt;span class="s"&gt;--name my-func-app \&lt;/span&gt;
        &lt;span class="s"&gt;--resource-group my-rg \&lt;/span&gt;
        &lt;span class="s"&gt;--slot staging \&lt;/span&gt;
        &lt;span class="s"&gt;--target-slot production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Swap gotchas
&lt;/h3&gt;

&lt;p&gt;Watch for these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Running functions are terminated&lt;/strong&gt; during a swap. There is no graceful drain. If you have long-running executions, they will be killed. For timer or queue triggers, the runtime will pick up incomplete work after the swap, but HTTP requests in flight will fail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warm-up matters.&lt;/strong&gt; After a swap, the new production instances need to initialize. Set &lt;code&gt;WEBSITE_SWAP_WARMUP_PING_PATH&lt;/code&gt; to an endpoint (like a health check) that forces initialization before traffic arrives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep app names under 32 characters.&lt;/strong&gt; Longer names can cause host ID collisions between slots, leading to unexpected behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Flex Consumption alternative: rolling updates
&lt;/h3&gt;

&lt;p&gt;Flex Consumption doesn't support slots, but it offers rolling updates as an alternative. With &lt;code&gt;siteUpdateStrategy.type&lt;/code&gt; set to &lt;code&gt;RollingUpdate&lt;/code&gt;, Azure replaces instances in batches rather than all at once, giving in-progress executions a 60-minute grace period to complete.&lt;/p&gt;

&lt;p&gt;The trade-off: there's no separate staging URL for validation, no way to split traffic between versions, and rollback means redeploying the previous version rather than an instant swap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment configuration in pipelines
&lt;/h2&gt;

&lt;p&gt;A deployment pipeline needs to put the right configuration in the right environment without leaking secrets into workflow files. GitHub Environments, the secrets hierarchy, and Key Vault references each handle a piece of this.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Environments
&lt;/h3&gt;

&lt;p&gt;Environments are configured under your repository's Settings &amp;gt; Environments. Each environment can have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Required reviewers&lt;/strong&gt; (up to 6 people who must approve before the deploy job runs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait timers&lt;/strong&gt; (a delay before deployment proceeds, useful for change windows)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment branches&lt;/strong&gt; (restrict which branches can target this environment)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the workflow, &lt;code&gt;environment: production&lt;/code&gt; on a job ties it to that environment's rules. The job will pause and wait for approval if reviewers are configured.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secrets hierarchy
&lt;/h3&gt;

&lt;p&gt;GitHub secrets exist at three levels:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnihqze1g59dtnn7jg4b4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnihqze1g59dtnn7jg4b4.png" alt="GitHub secrets hierarchy: Organization, Repository, and Environment levels with scope and examples" width="752" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the same secret name exists at multiple levels, &lt;strong&gt;environment wins over repository, which wins over organization&lt;/strong&gt;. This means you can set &lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt; at the environment level with different values for &lt;code&gt;development&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, and &lt;code&gt;production&lt;/code&gt;, each pointing to a different service principal scoped to its own resource group.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting app configuration during deployment
&lt;/h3&gt;

&lt;p&gt;Your Function App needs configuration values beyond what's in the code. The most direct approach is the Azure CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure app settings&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/cli@v2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inlineScript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;az functionapp config appsettings set \&lt;/span&gt;
        &lt;span class="s"&gt;--name ${{ vars.FUNCTION_APP_NAME }} \&lt;/span&gt;
        &lt;span class="s"&gt;--resource-group ${{ vars.RESOURCE_GROUP }} \&lt;/span&gt;
        &lt;span class="s"&gt;--settings \&lt;/span&gt;
          &lt;span class="s"&gt;"ServiceBus__Connection=${{ secrets.SERVICEBUS_CONNECTION }}" \&lt;/span&gt;
          &lt;span class="s"&gt;"FeatureFlags__NewCheckout=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;vars&lt;/code&gt; (GitHub Variables) for non-sensitive configuration and &lt;code&gt;secrets&lt;/code&gt; for anything you wouldn't put in a log file.&lt;/p&gt;

&lt;p&gt;One warning if you manage settings through Bicep or ARM templates instead: the ARM API &lt;strong&gt;replaces&lt;/strong&gt; all app settings on each deployment. If your template omits a setting that exists on the app, that setting gets deleted. The CLI's &lt;code&gt;appsettings set&lt;/code&gt; command merges instead, which is safer for incremental updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-environment workflow
&lt;/h3&gt;

&lt;p&gt;The build-once-deploy-many pattern chains environments with approval gates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="c1"&gt;# ... build steps from earlier ...&lt;/span&gt;

  &lt;span class="na"&gt;deploy-dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;development&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# download artifact, azure/login, functions-action&lt;/span&gt;
      &lt;span class="c1"&gt;# (same structure, different secrets per environment)&lt;/span&gt;

  &lt;span class="na"&gt;deploy-staging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy-dev&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;staging&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# deploy to staging slot, run smoke tests&lt;/span&gt;

  &lt;span class="na"&gt;deploy-production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy-staging&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;  &lt;span class="c1"&gt;# approval gate triggers here&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# swap staging to production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same artifact flows through all three environments. The only things that change are the secrets (different &lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt; per environment, each scoped to its own resource group) and the deployment target.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Vault integration
&lt;/h3&gt;

&lt;p&gt;This ties back to &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Part 6 (Configuration Done Right)&lt;/a&gt;. Instead of passing secret values through your pipeline, store them in Key Vault and reference them in app settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;ServiceBus__Connection&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;@Microsoft.KeyVault(VaultName=my-kv;SecretName=servicebus-conn)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your pipeline sets the &lt;strong&gt;reference&lt;/strong&gt;, not the secret value. The Function App's managed identity resolves the actual value at runtime using the &lt;code&gt;Key Vault Secrets User&lt;/code&gt; role. The pipeline never sees the secret, and rotating it in Key Vault takes effect without redeployment.&lt;/p&gt;

&lt;p&gt;If you use deployment slots, mark Key Vault references as slot settings when different environments need different secrets (e.g., staging points to a staging Key Vault, production to a production Key Vault).&lt;/p&gt;

&lt;h3&gt;
  
  
  What goes where
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftbxbgmxgtnorru9lrhhr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftbxbgmxgtnorru9lrhhr.png" alt="What goes where: GitHub Secrets for IDs, Key Vault for connection strings and keys, avoid secrets in YAML or Bicep" width="766" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Eight articles, one function app, and a pipeline that deploys itself. If something in your own setup doesn't match what's here, the series navigation links every piece: from the first HTTP trigger through testing to this deployment workflow.&lt;/p&gt;

&lt;p&gt;Do you deploy straight to production or use a staging slot? What made you choose one over the other?&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 8: Deploying to Azure: CI/CD with GitHub Actions (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>dotnet</category>
      <category>github</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Figuring out what actually needs a real Azure connection vs. what you can just test with a plain class… that’s where most testing headaches start. I wrote up how I handle it: unit tests, Testcontainers + Azurite, and full func start pipelines.</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Sat, 21 Mar 2026 06:16:23 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/figuring-out-what-actually-needs-a-real-azure-connection-vs-what-you-can-just-test-with-a-plain-548</link>
      <guid>https://dev.to/martin_oehlert/figuring-out-what-actually-needs-a-real-azure-connection-vs-what-you-can-just-test-with-a-plain-548</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml" class="crayons-story__hidden-navigation-link"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/martin_oehlert" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1661015%2Fd0bdf508-0244-49d8-8655-aea054d71b86.png" alt="martin_oehlert profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/martin_oehlert" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Martin Oehlert
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Martin Oehlert
                
              
              &lt;div id="story-author-preview-content-3375218" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/martin_oehlert" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1661015%2Fd0bdf508-0244-49d8-8655-aea054d71b86.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Martin Oehlert&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Mar 20&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml" id="article-link-3375218"&gt;
          Testing Azure Functions: Unit, Integration, and Local
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/azure"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;azure&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/azurefunctions"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;azurefunctions&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/serverless"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;serverless&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/dotnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;dotnet&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt; reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            15 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>serverless</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Testing Azure Functions: Unit, Integration, and Local</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 20 Mar 2026 07:17:37 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml</link>
      <guid>https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml</guid>
      <description>&lt;p&gt;Where do you draw the line between what needs a full Azure connection and what can be tested with a plain class instantiation? The isolated worker model makes the answer concrete: the function class is just wiring. Everything testable lives in a service class that knows nothing about Azure.&lt;/p&gt;

&lt;p&gt;Most testing pain comes from not drawing that line early enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design decision that makes testing possible
&lt;/h2&gt;

&lt;p&gt;Consider a function that does its own work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SqlConnection&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CreateOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthorizationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Anonymous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;CreateOrderRequest&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BadRequestObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Quantity must be greater than zero"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ORD-"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"N"&lt;/span&gt;&lt;span class="p"&gt;)[..&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"INSERT INTO Orders (OrderId, ProductId, Quantity) VALUES (@OrderId, @ProductId, @Quantity)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Created order {OrderId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreatedResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"/orders/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To unit test this, you need a real &lt;code&gt;SqlConnection&lt;/code&gt;. That means a real database, which means either a running SQL Server, Testcontainers, or a brittle in-memory substitute. Every test becomes an infrastructure test, even for something as simple as verifying that a zero quantity returns 400.&lt;/p&gt;

&lt;p&gt;The fix is to move the logic into a service class, leaving the function with nothing to do except call the service and map the result to a response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IOrderService&lt;/span&gt; &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CreateOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthorizationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Anonymous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;CreateOrderRequest&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BadRequestObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreatedResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"/orders/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function class is now three lines of routing logic. &lt;code&gt;IOrderService&lt;/code&gt; is a plain interface with no Azure types, no infrastructure, nothing that requires a running host to instantiate.&lt;/p&gt;

&lt;p&gt;This gives you two separate test targets. The service holds the logic and gets fast, isolated unit tests with no framework setup. The function class holds the routing and gets a thin layer of tests that verify the HTTP response shapes. Each layer can be tested on its own terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unit testing the service layer
&lt;/h2&gt;

&lt;p&gt;The service has one dependency worth injecting for tests: &lt;code&gt;IOrderRepository&lt;/code&gt;. Here's the full service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IOrderRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IOrderService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateOrderRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Quantity must be greater than zero"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"ORD-"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"N"&lt;/span&gt;&lt;span class="p"&gt;)[..&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Created order {OrderId} for {ProductId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test it, you need xUnit and NSubstitute. The &lt;code&gt;.csproj&lt;/code&gt; is minimal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Project&lt;/span&gt; &lt;span class="na"&gt;Sdk=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.NET.Sdk"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;IsTestProject&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/IsTestProject&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Test method names use underscores by convention (MethodName_Condition_Expected) --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;NoWarn&amp;gt;&lt;/span&gt;$(NoWarn);CA1707&lt;span class="nt"&gt;&amp;lt;/NoWarn&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;FrameworkReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.AspNetCore.App"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.NET.Test.Sdk"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"NSubstitute"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"xunit"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"xunit.runner.visualstudio"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;PrivateAssets&amp;gt;&lt;/span&gt;all&lt;span class="nt"&gt;&amp;lt;/PrivateAssets&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;IncludeAssets&amp;gt;&lt;/span&gt;runtime; build; native; contentfiles; analyzers&lt;span class="nt"&gt;&amp;lt;/IncludeAssets&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/PackageReference&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ProjectReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"../HttpTriggerDemo/HttpTriggerDemo.csproj"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests themselves need no Azure infrastructure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderServiceTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IOrderRepository&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Substitute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;For&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOrderRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;OrderService&lt;/span&gt; &lt;span class="n"&gt;_service&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderServiceTests&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NullLogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderAsync_WithValidRequest_ReturnsSuccess&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;True&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSuccess&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderAsync_WithValidRequest_SavesOrderToRepository&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Received&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Arg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Theory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineData&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineData&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderAsync_WithInvalidQuantity_ReturnsFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;False&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSuccess&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Theory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineData&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderAsync_WithInvalidQuantity_DoesNotCallRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DidNotReceive&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Arg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;NullLogger&amp;lt;T&amp;gt;.Instance&lt;/code&gt; is the right choice for service tests. You are testing behavior, not logging output. Using &lt;code&gt;Substitute.For&amp;lt;ILogger&amp;lt;T&amp;gt;&amp;gt;()&lt;/code&gt; to verify that specific log messages were emitted is a fragile approach: log messages are implementation details that change often and aren't part of the service contract. Save NSubstitute for dependencies whose behavior actually matters to the test outcome, like &lt;code&gt;IOrderRepository&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[Theory]&lt;/code&gt; + &lt;code&gt;[InlineData]&lt;/code&gt; handles the validation branches without duplicating test body. Each &lt;code&gt;InlineData&lt;/code&gt; value runs as a separate test in the output, so you get clear signal on exactly which inputs fail. The two &lt;code&gt;[Theory]&lt;/code&gt; blocks above run 3 + 2 = 5 test cases from a handful of lines.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Received()&lt;/code&gt; and &lt;code&gt;DidNotReceive()&lt;/code&gt; are NSubstitute's call-count assertions. The second &lt;code&gt;[Fact]&lt;/code&gt; verifies the repository was called with the right data; the second &lt;code&gt;[Theory]&lt;/code&gt; verifies it was never called when validation fails. Together they cover both the happy path and the guard clause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unit testing the function class
&lt;/h2&gt;

&lt;p&gt;When you use &lt;code&gt;ConfigureFunctionsWebApplication()&lt;/code&gt; (the ASP.NET Core integration mode), the function's &lt;code&gt;HttpRequest&lt;/code&gt; is a standard ASP.NET Core &lt;code&gt;HttpRequest&lt;/code&gt;. That means you can construct a &lt;code&gt;DefaultHttpContext&lt;/code&gt; in tests and pass &lt;code&gt;context.Request&lt;/code&gt; directly to the function method, with no Functions runtime involved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderFunctionTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IOrderService&lt;/span&gt; &lt;span class="n"&gt;_orderService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Substitute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;For&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;OrderFunction&lt;/span&gt; &lt;span class="n"&gt;_function&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderFunctionTests&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_function&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_orderService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateOrder_WhenServiceSucceeds_Returns201Created&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ORD-ABCD1234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DefaultHttpContext&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreatedResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/orders/ORD-ABCD1234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateOrder_WhenServiceFails_Returns400BadRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Quantity must be greater than zero"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DefaultHttpContext&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BadRequestObjectResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Quantity must be greater than zero"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what these tests do not cover: the &lt;code&gt;[HttpTrigger]&lt;/code&gt; attribute, binding resolution, middleware, or anything the Functions host owns. That's intentional. The function's responsibility is to map a service result to an HTTP response. Two tests cover both outcome branches. Anything beyond that is integration territory.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;[HttpTrigger]&lt;/code&gt; and &lt;code&gt;[FromBody]&lt;/code&gt; attributes are metadata for the runtime. They don't execute during a direct method call, so there's nothing to test or mock.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timer triggers
&lt;/h3&gt;

&lt;p&gt;Timer functions follow the same pattern. &lt;code&gt;TimerInfo&lt;/code&gt; is a concrete class from the Functions SDK with settable properties, so you construct it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CleanupFunctionTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;CleanupFunction&lt;/span&gt; &lt;span class="n"&gt;_function&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NullLogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CleanupFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run_WhenOnSchedule_CompletesWithoutError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TimerInfo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;IsPastDue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// No exception thrown = the function handled the timer correctly.&lt;/span&gt;
        &lt;span class="c1"&gt;// Timer functions have no return value — the observable outcome is&lt;/span&gt;
        &lt;span class="c1"&gt;// either successful completion or an exception.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run_WhenPastDue_StillCompletes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TimerInfo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;IsPastDue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Timer function tests are often this minimal. The function's behavior on &lt;code&gt;IsPastDue = true&lt;/code&gt; is to log a warning; there's no meaningful return value to assert on. What you're verifying is that the function reaches completion without throwing, and that the &lt;code&gt;IsPastDue&lt;/code&gt; branch doesn't break anything. If your cleanup function does real work (deleting records, archiving blobs), that work lives in a service and gets tested through the service tests, not through the function.&lt;/p&gt;




&lt;h2&gt;
  
  
  Integration testing with Testcontainers
&lt;/h2&gt;

&lt;p&gt;Unit tests get you 80% of the way. They don't verify that DI registrations are correct, that your database schema matches your queries, or that a real &lt;code&gt;TableClient&lt;/code&gt; call actually persists what you think it does. That's two separate problems: the composition root, and the data layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying the composition root
&lt;/h3&gt;

&lt;p&gt;The first failure mode is silent: a service registration is missing, and &lt;code&gt;OrderFunction&lt;/code&gt;'s constructor throws at runtime while every unit test passes. A composition test catches this without Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HostIntegrationTests&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAsyncLifetime&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IHost&lt;/span&gt; &lt;span class="n"&gt;_host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;!;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsWebApplication&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// WorkerHostedService opens a gRPC channel to the Functions host.&lt;/span&gt;
                &lt;span class="c1"&gt;// That host doesn't exist in tests — remove it or the build hangs.&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ImplementationType&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"WorkerHostedService"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;IOrderService_ResolvesFromDi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StopAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WebApplicationFactory&amp;lt;Program&amp;gt;&lt;/code&gt; fails with Azure Functions isolated worker. The model uses gRPC for host-worker communication, and the factory hits a channel URI parsing error when no Functions host is running. The &lt;code&gt;HostBuilder&lt;/code&gt; approach mirrors &lt;code&gt;Program.cs&lt;/code&gt; exactly, with the gRPC listener stripped. This test doesn't call any function logic; it only verifies the container compiles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing the data layer
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;InMemoryOrderRepository&lt;/code&gt; lets unit tests run fast, but it tells you nothing about whether your actual persistence works. A production implementation using Azure Table Storage looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TableStorageOrderRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TableClient&lt;/span&gt; &lt;span class="n"&gt;tableClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IOrderRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TableEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Quantity"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tableClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEntityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The integration test spins up Azurite in Docker via Testcontainers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TableStorageOrderRepositoryTests&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAsyncLifetime&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AzuriteContainer&lt;/span&gt; &lt;span class="n"&gt;_azurite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AzuriteBuilder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_azurite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SaveAsync_WithValidOrder_PersistsToTableStorage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TableClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_azurite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateIfNotExistsAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TableStorageOrderRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ORD-TEST01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetEntityAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TableEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Quantity"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_azurite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add one package to the test project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Testcontainers.Azurite"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each test run gets a fresh container. No ports to reserve, no cleanup between runs; Testcontainers handles port allocation for parallel CI execution automatically.&lt;/p&gt;

&lt;p&gt;The same pattern covers blob and queue operations. For relational databases, &lt;code&gt;Testcontainers.MsSql&lt;/code&gt; and &lt;code&gt;Testcontainers.PostgreSql&lt;/code&gt; provide the same lifecycle wrapper for SQL Server and Postgres.&lt;/p&gt;




&lt;h2&gt;
  
  
  Local E2E testing with &lt;code&gt;func start&lt;/code&gt; + Azurite
&lt;/h2&gt;

&lt;p&gt;Logic tests cover &lt;code&gt;OrderService&lt;/code&gt;. Repository tests cover &lt;code&gt;TableStorageOrderRepository&lt;/code&gt;. Neither covers what happens when the Functions host receives an HTTP request, routes it through middleware, deserializes the body, calls the function, and returns a response.&lt;/p&gt;

&lt;p&gt;For that, the host must be running. The approach is to start Azurite and &lt;code&gt;func start&lt;/code&gt; together in the test fixture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FunctionsE2ETests&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAsyncLifetime&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AzuriteContainer&lt;/span&gt; &lt;span class="n"&gt;_azurite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AzuriteBuilder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_funcProcess&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_azurite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;_funcProcess&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ProcessStartInfo&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;FileName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"func"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Arguments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"start --port 7071"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;WorkingDirectory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFullPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"../../../HttpTriggerDemo"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;EnvironmentVariables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_azurite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"FUNCTIONS_WORKER_RUNTIME"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"dotnet-isolated"&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;RedirectStandardOutput&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;UseShellExecute&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;WaitForHostReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_funcProcess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateOrder_WithValidRequest_Returns201&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostAsJsonAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"http://localhost:7071/api/orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WIDGET-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Created&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;WaitForHostReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TaskCompletionSource&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputDataReceived&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Host started"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrySetResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginOutputReadLine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_funcProcess&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entireProcessTree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_azurite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;func&lt;/code&gt; must be on the PATH. CI pipelines need &lt;code&gt;npm install -g azure-functions-core-tools@4&lt;/code&gt; before these tests run: it's a test infrastructure dependency that bites if you assume it's there.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Kill(entireProcessTree: true)&lt;/code&gt; matters on Windows. &lt;code&gt;func start&lt;/code&gt; spawns child processes; killing just the parent leaves orphaned processes holding port 7071, which causes every subsequent E2E test run in that session to hang on startup.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WaitForHostReady&lt;/code&gt; polls stdout for "Host started". Startup takes 3-10 seconds depending on cold JIT and machine speed. Set the timeout conservatively: a flaky timeout is harder to debug than a slow test.&lt;/p&gt;

&lt;p&gt;Put these tests in a separate project with &lt;code&gt;[Trait("Category", "E2E")]&lt;/code&gt; and exclude them from the fast inner development loop. They're most useful in CI as a gate before deployment, not as daily feedback during development.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing an event-driven function
&lt;/h2&gt;

&lt;p&gt;HTTP triggers test cleanly: call the function directly, inspect the return value. Event Hub triggers are different. The function receives a batch of &lt;code&gt;EventData&lt;/code&gt;, deserializes each message, and delegates to a service; the trigger binding itself is provided by the runtime. That runtime can run locally.&lt;/p&gt;

&lt;p&gt;The scenario here is an IoT pipeline: devices publish sensor readings to an Event Hub, and a function consumes the batch, validates each reading, and writes to Cosmos DB.&lt;/p&gt;

&lt;h3&gt;
  
  
  The function
&lt;/h3&gt;

&lt;p&gt;The function stays thin. Deserialize the batch, call the service, nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SensorReadingFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SensorReadingFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ISensorProcessor&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SensorReadingFunction&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;EventHubTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sensor-readings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"EventHubConnection"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="n"&gt;EventData&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Processing batch of {Count} events"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;eventData&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reading&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SensorReading&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;eventData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Span&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;EventData[]&lt;/code&gt; parameter receives the batch. The function doesn't know or care how many partitions the hub has, how messages were routed, or what retry policy applies: that's the runtime's job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unit testing the function
&lt;/h3&gt;

&lt;p&gt;Construct &lt;code&gt;EventData&lt;/code&gt; directly with a JSON body and call &lt;code&gt;Run()&lt;/code&gt;. No containers, no emulator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run_WithBatchOfThreeEvents_CallsProcessorThreeTimes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Substitute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;For&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ISensorProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SensorReadingFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NullLogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SensorReadingFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;EventData&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;CreateEventData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SensorReading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"device-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;22.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;60.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="nf"&gt;CreateEventData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SensorReading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"device-02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;25.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;55.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="nf"&gt;CreateEventData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SensorReading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"device-03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;18.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;72.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Received&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ProcessAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Arg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SensorReading&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;EventData&lt;/span&gt; &lt;span class="nf"&gt;CreateEventData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SensorReading&lt;/span&gt; &lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeToUtf8Bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches deserialization bugs and verifies the batch loop without starting anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full trigger integration test
&lt;/h3&gt;

&lt;p&gt;Unit tests verify the dispatch and deserialization logic in isolation. The full pipeline test goes further: a real message flows from Event Hubs through the function and into Cosmos DB.&lt;/p&gt;

&lt;p&gt;The fixture starts three containers in parallel (Azurite for the Functions runtime, the Event Hubs emulator, and the Cosmos DB emulator), then launches &lt;code&gt;func start&lt;/code&gt; as a child process wired to all three. The full source is in &lt;a href="https://github.com/martinoehlert/azure-functions-samples/blob/main/EventHubDemo.Tests/SensorPipelineFixture.cs" rel="noopener noreferrer"&gt;SensorPipelineFixture.cs&lt;/a&gt;. The container declarations and process wiring both require non-obvious configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Container configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Use latest: Core Tools 4.8 sends an API version that Azurite 3.28.0 (the Testcontainers default) rejects.&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AzuriteContainer&lt;/span&gt; &lt;span class="n"&gt;_azurite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AzuriteBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mcr.microsoft.com/azure-storage/azurite:latest"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// WithPortBinding pins the host port so localhost:8081 resolves from the func child process.&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;CosmosDbContainer&lt;/span&gt; &lt;span class="n"&gt;_cosmos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CosmosDbBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithPortBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8081&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;8081&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithWaitStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ForUnixContainer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UntilMessageIsLogged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Gateway=OK, Explorer=OK"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;EventHubsContainer&lt;/span&gt; &lt;span class="n"&gt;_eventHubs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;SensorPipelineFixture&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_eventHubs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;EventHubsBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithAcceptLicenseAgreement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithConfigurationBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EventHubsServiceConfiguration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sensor-readings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithWaitStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ForUnixContainer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UntilMessageIsLogged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Emulator Service is Successfully Up!"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Testcontainers pins &lt;code&gt;azurite:3.28.0&lt;/code&gt; as its default. Azure Functions Core Tools 4.8 sends API version &lt;code&gt;2024-08-04&lt;/code&gt;; Azurite 3.28.0 rejects that version with a 400. Pinning to &lt;code&gt;latest&lt;/code&gt; resolves it.&lt;/p&gt;

&lt;p&gt;Both the Event Hubs emulator and the Cosmos &lt;code&gt;vnext-preview&lt;/code&gt; image are distroless: no shell, no &lt;code&gt;/bin/sh&lt;/code&gt;. The default Testcontainers port-check wait strategy execs &lt;code&gt;/bin/sh&lt;/code&gt; inside the container to verify the port is listening. On a distroless image, that exec fails and the strategy hangs indefinitely. &lt;code&gt;UntilMessageIsLogged()&lt;/code&gt; watches the container's stdout stream directly, bypassing the shell dependency.&lt;/p&gt;

&lt;p&gt;The Cosmos emulator returns its own internal address in the account metadata it sends back to clients. The test-process &lt;code&gt;CosmosClient&lt;/code&gt; receives &lt;code&gt;localhost:8081&lt;/code&gt; as the endpoint and follows it there. &lt;code&gt;WithPortBinding(8081, 8081)&lt;/code&gt; ensures that host port is pinned, so the &lt;code&gt;func&lt;/code&gt; child process (which constructs its own &lt;code&gt;CosmosClient&lt;/code&gt;) lands on the same address.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WithResourceMapping&lt;/code&gt; mounts a JSON configuration file into the Event Hubs emulator container, but it doesn't set the &lt;code&gt;ServiceConfiguration&lt;/code&gt; property the builder reads at &lt;code&gt;Build()&lt;/code&gt; time. The build throws at runtime. &lt;code&gt;WithConfigurationBuilder&lt;/code&gt; uses the fluent API to set &lt;code&gt;ServiceConfiguration&lt;/code&gt; directly, and the configuration is validated at build time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Process wiring:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cosmosPort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_cosmos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetMappedPublicPort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8081&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cosmosKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_cosmos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;';'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;First&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AccountKey="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordinal&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AccountKey="&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;CosmosClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CosmosClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_cosmos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CosmosClientOptions&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ConnectionMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ConnectionMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Gateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;HttpClientFactory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CosmosEmulatorHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cosmosPort&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="n"&gt;SerializerOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CosmosSerializationOptions&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;PropertyNamingPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CosmosPropertyNamingPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CamelCase&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;startInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"CosmosDbConnection"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="s"&gt;$"AccountEndpoint=http://localhost:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cosmosPort&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/;AccountKey=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cosmosKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;func&lt;/code&gt; child process constructs its own &lt;code&gt;CosmosClient&lt;/code&gt; from the &lt;code&gt;CosmosDbConnection&lt;/code&gt; environment variable; it can't share the test process's &lt;code&gt;HttpClient&lt;/code&gt; handler across the process boundary. Passing &lt;code&gt;AccountEndpoint=http://localhost:{port}/&lt;/code&gt; with an explicitly extracted key gives the child process a direct HTTP connection to the emulator without needing the handler.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CosmosEmulatorHandler&lt;/code&gt; is an &lt;code&gt;HttpMessageHandler&lt;/code&gt; that rewrites outgoing requests from the emulator's self-reported internal hostname to &lt;code&gt;localhost:{cosmosPort}&lt;/code&gt;. Without it, the SDK follows the internal address the emulator returns in its account metadata and misses the container.&lt;/p&gt;

&lt;p&gt;The full fixture also implements &lt;code&gt;WaitForFunctionsHostAsync&lt;/code&gt; (polls &lt;code&gt;localhost:7071/admin/host/status&lt;/code&gt; until the host responds) and &lt;code&gt;DisposeAsync&lt;/code&gt; (kills the process tree and disposes all three containers). Both are in the &lt;a href="https://github.com/martinoehlert/azure-functions-samples/blob/main/EventHubDemo.Tests/SensorPipelineFixture.cs" rel="noopener noreferrer"&gt;full source&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The test publishes a batch and polls Cosmos DB until the document appears:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SensorPipelineFixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SensorPipelineTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SensorPipelineFixture&lt;/span&gt; &lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;PublishedEvent_WithValidReading_AppearsInCosmosDb&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reading&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SensorReading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;DeviceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$"device-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;23.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Humidity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;58.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProducerClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBatchAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;EventData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeToUtf8Bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProducerClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CosmosClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetContainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SensorData"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"readings"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetItemQueryIterator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
                &lt;span class="s"&gt;$"SELECT * FROM c WHERE c.deviceId = '&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeviceId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasMoreResults&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadNextAsync&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;23.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fixture takes 60–90 seconds to start. Run it separately from unit tests in CI using xUnit's &lt;code&gt;[Collection]&lt;/code&gt; trait or a test filter.&lt;/p&gt;

&lt;p&gt;Add the packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Testcontainers.EventHubs"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Testcontainers.CosmosDb"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Testcontainers.Azurite"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Azure.Messaging.EventHubs"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Newtonsoft.Json"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cosmos SDK v3 requires Newtonsoft.Json at runtime via an internal dependency. Omitting it produces a &lt;code&gt;FileNotFoundException&lt;/code&gt; at startup with no message connecting it to Cosmos.&lt;/p&gt;




&lt;h2&gt;
  
  
  What can't be emulated locally
&lt;/h2&gt;

&lt;p&gt;Azurite and &lt;code&gt;func start&lt;/code&gt; cover wiring and trigger dispatch. Some behaviors only emerge in Azure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cold starts.&lt;/strong&gt; Local tests keep the host warm throughout the run. Consumption plan cold starts in Azure hit 500ms-2s for .NET depending on deployment size. If your SLA depends on p99 latency, that gap only shows in production traffic — local tests give you no signal on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managed identity credential resolution.&lt;/strong&gt; &lt;code&gt;DefaultAzureCredential&lt;/code&gt; falls through a chain of credential sources. Locally it uses developer machine credentials or environment variables. In Azure it uses the Managed Identity endpoint. A misconfigured client ID or missing role assignment won't surface until the function runs with a real identity attached. The local credential chain doesn't exercise the same code path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scale-out behavior.&lt;/strong&gt; &lt;code&gt;func start&lt;/code&gt; runs one worker. Azure scales to N workers based on trigger backlog. Race conditions, partition contention, and shared-state bugs appear only under concurrent load across multiple instances. No local setup replicates this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KEDA-based scaling decisions.&lt;/strong&gt; Event Hub and Service Bus triggers scale based on message lag, but the scaling decisions come from the infrastructure, not the worker process. There's no local equivalent for how Azure routes partitions across workers as instances scale up.&lt;/p&gt;

&lt;p&gt;The useful takeaway: unit tests and integration tests give fast, reliable feedback on logic and wiring. They don't give confidence about latency under cold conditions, behavior at scale, or cloud-managed auth. Build those signals from production observability (Application Insights, structured logs, alert rules), not from test infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Patterns that cause pain
&lt;/h2&gt;

&lt;p&gt;A few mistakes appear repeatedly in Azure Functions test suites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Asserting on log messages.&lt;/strong&gt; &lt;code&gt;Substitute.For&amp;lt;ILogger&amp;lt;T&amp;gt;&amp;gt;()&lt;/code&gt; lets you verify that specific log calls were made. Don't. Log messages are implementation details: they change wording, get split into multiple calls, or get removed during refactoring. When they do, your test breaks without any behavior change. Use &lt;code&gt;NullLogger&amp;lt;T&amp;gt;.Instance&lt;/code&gt; for services and only substitute loggers when logging output is the actual behavior under test (which is almost never).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reaching into the runtime from unit tests.&lt;/strong&gt; &lt;code&gt;[HttpTrigger]&lt;/code&gt;, &lt;code&gt;[FromBody]&lt;/code&gt;, and &lt;code&gt;[QueueTrigger]&lt;/code&gt; are metadata for the runtime to read. They don't execute during a direct method call. Trying to test that binding attributes are present, or that the runtime would route correctly, puts you in the business of testing the Functions SDK rather than your code. The routing table lives in the host config; your job is to test what happens once the host calls your method.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using constructors for container lifecycle.&lt;/strong&gt; xUnit creates test class instances before running tests, but &lt;code&gt;StartAsync()&lt;/code&gt; is async. Initializing a &lt;code&gt;AzuriteContainer&lt;/code&gt; in a constructor blocks the thread and causes tests to hang silently. Always use &lt;code&gt;IAsyncLifetime&lt;/code&gt;: &lt;code&gt;InitializeAsync&lt;/code&gt; for startup, &lt;code&gt;DisposeAsync&lt;/code&gt; for teardown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing the service layer twice.&lt;/strong&gt; Once you have thorough &lt;code&gt;OrderServiceTests&lt;/code&gt;, the function-level tests (&lt;code&gt;OrderFunctionTests&lt;/code&gt;) should only cover the HTTP response mapping: does a successful result return 201, does a failure return 400. Repeating the validation and business logic assertions at the function level creates duplicate coverage that breaks together whenever the service contract changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing your testing strategy
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu2d2hp69aadyyzdcj8au.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu2d2hp69aadyyzdcj8au.png" alt="Azure Functions Testing Pyramid" width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Infrastructure needed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Service logic&lt;/td&gt;
&lt;td&gt;Unit test&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Function routing&lt;/td&gt;
&lt;td&gt;Unit test&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DI wiring + middleware&lt;/td&gt;
&lt;td&gt;HostBuilder trick&lt;/td&gt;
&lt;td&gt;None (gRPC stripped)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data layer round-trips&lt;/td&gt;
&lt;td&gt;Testcontainers (SQL/Postgres/Azurite)&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trigger dispatch&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;func start&lt;/code&gt; + Azurite&lt;/td&gt;
&lt;td&gt;Core Tools + Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full pipeline&lt;/td&gt;
&lt;td&gt;Testcontainers Docker image&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start from the top and stop as soon as the tests cover the risk you're managing. For most business logic, unit tests against the service layer are enough. The function class tests add a few minutes of coverage for the HTTP response shapes. Integration and E2E tests are worth the infrastructure cost only when you need to verify wiring, real database behavior, or trigger dispatch.&lt;/p&gt;




&lt;p&gt;Do you unit test your function class directly, or do you treat the service layer as the boundary and skip function-level tests entirely?&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 7: Testing Azure Functions: Unit, Integration, and Local (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>serverless</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Configuration Done Right: Settings, Secrets, and Key Vault</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 13 Mar 2026 05:43:09 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h</link>
      <guid>https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h</guid>
      <description>&lt;p&gt;You add a Service Bus connection string to &lt;code&gt;appsettings.json&lt;/code&gt;. You deploy. The trigger fails at startup with something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Microsoft.Azure.WebJobs.Host.Listeners.FunctionListenerException:
The listener for function 'ProcessMessage' was unable to start.
Microsoft.Azure.WebJobs.ServiceBus.Listeners.ServiceBusListener:
Connection string not found.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your code reads it fine. But the trigger cannot start because the &lt;em&gt;host&lt;/em&gt; never sees &lt;code&gt;appsettings.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Azure Functions has two distinct configuration surfaces that solve different problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;host process&lt;/strong&gt; (&lt;code&gt;func.exe&lt;/code&gt;) resolves trigger and binding connections from environment variables.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;worker process&lt;/strong&gt; (your .NET code) reads &lt;code&gt;IConfiguration&lt;/code&gt;, which includes &lt;code&gt;appsettings.json&lt;/code&gt;, environment variables, and any other sources you add.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These two processes share environment variables but nothing else. This article covers how to configure each correctly, how to move secrets to Key Vault without changing your code, and how to use the options pattern for strongly-typed settings.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw5cmfv8pfwiwwbjpa91x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw5cmfv8pfwiwwbjpa91x.png" alt="Azure Functions configuration flow diagram showing local.settings.json and Azure App Settings flowing through environment variables to the host and worker processes, with Key Vault references resolved at startup" width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All code from this article is available in the &lt;a href="https://github.com/MO2k4/azure-functions-samples" rel="noopener noreferrer"&gt;azure-functions-samples&lt;/a&gt; repository under &lt;code&gt;ConfigurationDemo/&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  local.settings.json
&lt;/h2&gt;

&lt;p&gt;In local development, &lt;code&gt;local.settings.json&lt;/code&gt; is how you supply environment variables to both processes simultaneously. Core Tools (&lt;code&gt;func.exe&lt;/code&gt;) reads this file at startup and injects every entry in the &lt;code&gt;Values&lt;/code&gt; section as a process environment variable before launching the worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"IsEncrypted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UseDevelopmentStorage=true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"FUNCTIONS_WORKER_RUNTIME"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotnet-isolated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ServiceBusConnection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Endpoint=sb://..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Api__BaseUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Api__TimeoutSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything in &lt;code&gt;Values&lt;/code&gt; is a flat string pair, no nesting. For hierarchical settings, use double underscore as the separator: &lt;code&gt;Api__BaseUrl&lt;/code&gt; becomes &lt;code&gt;Api:BaseUrl&lt;/code&gt; in &lt;code&gt;IConfiguration&lt;/code&gt;. All values are strings; the configuration binder converts them to the correct type when binding to options classes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;ConnectionStrings&lt;/code&gt; section is a trap.&lt;/strong&gt; It exists for ORMs like Entity Framework that expect connection strings under &lt;code&gt;ConnectionStrings:*&lt;/code&gt;. Core Tools loads these with a &lt;code&gt;ConnectionStrings:&lt;/code&gt; prefix — so &lt;code&gt;ConnectionStrings.MyDb&lt;/code&gt; becomes the environment variable &lt;code&gt;ConnectionStrings:MyDb&lt;/code&gt;. The Functions host looks for binding connections by their bare name. Put a Service Bus or Storage connection string in &lt;code&gt;ConnectionStrings&lt;/code&gt; and the trigger cannot find it, even though your application code can read it fine with &lt;code&gt;configuration.GetConnectionString("MyDb")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rule: trigger and binding connections go in &lt;code&gt;Values&lt;/code&gt;, always.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Azure ignores this file entirely.&lt;/strong&gt; It is consumed only by Core Tools locally. When you deploy, you configure settings separately in the portal, via CLI, or in Bicep. If you use &lt;code&gt;func azure functionapp publish --publish-local-settings&lt;/code&gt;, only the &lt;code&gt;Values&lt;/code&gt; section is copied. The &lt;code&gt;ConnectionStrings&lt;/code&gt; section is never published — another reason to avoid it.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;local.settings.json&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;. The Functions project template does this automatically, but verify it before your first commit. This file will contain connection strings, API keys, and storage credentials.&lt;/p&gt;




&lt;h2&gt;
  
  
  Azure App Settings
&lt;/h2&gt;

&lt;p&gt;In Azure, every Application Setting is an environment variable injected into the host process. The Functions runtime treats them identically to what &lt;code&gt;local.settings.json&lt;/code&gt; provides locally.&lt;/p&gt;

&lt;p&gt;The portal lists them under &lt;strong&gt;Settings &amp;gt; Environment Variables &amp;gt; App settings&lt;/strong&gt;. Values are encrypted at rest and masked in the UI. Any change to Application Settings causes the function app to restart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the Application Settings blade, not the Connection strings blade.&lt;/strong&gt; The Connection strings section in the portal adds a type prefix to the environment variable name. A custom connection string named &lt;code&gt;ServiceBus&lt;/code&gt; becomes &lt;code&gt;CUSTOMCONNSTR_ServiceBus&lt;/code&gt; in the environment. The Service Bus trigger looking for &lt;code&gt;ServiceBus&lt;/code&gt; will not find it.&lt;/p&gt;

&lt;p&gt;The Connection strings portal section exists for ASP.NET compatibility. For Azure Functions, put everything in Application Settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hierarchical settings
&lt;/h3&gt;

&lt;p&gt;Flat environment variables do not support nesting. The convention on Azure (which runs on Linux) is to use double underscore as the hierarchy separator:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App Setting name&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;IConfiguration&lt;/code&gt; key&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Api__BaseUrl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Api:BaseUrl&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Api__TimeoutSeconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Api:TimeoutSeconds&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Colon (&lt;code&gt;:&lt;/code&gt;) works as a separator on Windows only. Double underscore works on both platforms. Always use &lt;code&gt;__&lt;/code&gt; in App Setting names to ensure your functions run correctly whether the app is on a Windows or Linux hosting plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slot settings
&lt;/h3&gt;

&lt;p&gt;Deployment slots each have their own App Settings. By default, settings swap along with the code when you swap slots. &lt;strong&gt;Slot settings&lt;/strong&gt; (sticky settings) stay with the slot and do not swap.&lt;/p&gt;

&lt;p&gt;The common use case: staging and production connect to different databases or service bus namespaces. Mark those connection strings as slot settings so a staging deployment cannot accidentally point production code at the staging database.&lt;/p&gt;

&lt;p&gt;To mark a setting as sticky, check &lt;strong&gt;Deployment slot setting&lt;/strong&gt; when editing it in the portal. In Bicep, set &lt;code&gt;"slotSetting": true&lt;/code&gt; on the app setting object.&lt;/p&gt;

&lt;p&gt;One gotcha: a sticky setting must exist in every slot involved in a swap. If a sticky setting exists in staging but not in production, and you swap, the setting disappears from staging after the swap. Create the setting (with the appropriate value for each environment) in every slot before you start using sticky settings.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Vault references
&lt;/h2&gt;

&lt;p&gt;Key Vault references let you store secrets in Azure Key Vault and reference them from App Settings. The Functions host resolves the reference at startup. Your code reads the setting with the same &lt;code&gt;IConfiguration["MyKey"]&lt;/code&gt; call it has always used.&lt;/p&gt;

&lt;p&gt;The reference syntax in the App Setting value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/ApiKey/)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or by name, if the Key Vault is in the same subscription:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Microsoft.KeyVault(VaultName=myvault;SecretName=ApiKey)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both forms produce identical behavior at runtime. Omit the secret version from the URI to always get the latest version. The platform caches the resolved value and re-fetches it every 24 hours. Rotating a secret takes effect automatically within that window without a deployment or restart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failed references are silent.&lt;/strong&gt; If the reference cannot be resolved (wrong vault name, missing permissions, deleted secret), the App Setting receives the literal reference string as its value: &lt;code&gt;@Microsoft.KeyVault(...)&lt;/code&gt;. This propagates through to your code, which typically throws because it receives an unexpected format instead of the secret value. To diagnose: open the setting in the portal and look for an error status indicator in the edit dialog. In the Azure portal, Platform features &amp;gt; Diagnose and solve problems also has a Key Vault Application Settings Diagnostics detector.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managed identity setup
&lt;/h3&gt;

&lt;p&gt;The function app needs permission to read secrets from the vault. The recommended approach is a system-assigned managed identity.&lt;/p&gt;

&lt;p&gt;Enable it in the portal under &lt;strong&gt;Settings &amp;gt; Identity &amp;gt; System assigned &amp;gt; Status: On&lt;/strong&gt;. Via CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az webapp identity assign &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &amp;lt;app-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then assign the &lt;strong&gt;Key Vault Secrets User&lt;/strong&gt; role to the identity on the vault:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az role assignment create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt; &lt;span class="s2"&gt;"Key Vault Secrets User"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assignee&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;principalId&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"/subscriptions/&amp;lt;sub&amp;gt;/resourceGroups/&amp;lt;rg&amp;gt;/providers/Microsoft.KeyVault/vaults/&amp;lt;vault-name&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--assignee&lt;/code&gt; value is the &lt;code&gt;principalId&lt;/code&gt; from the identity assignment output, not the client ID and not the app's resource ID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Vault Secrets User, not Contributor.&lt;/strong&gt; The Contributor role manages the vault as an Azure resource (creating, deleting, modifying it). It does not grant access to read secret values. Key Vault Secrets User grants data-plane read access to secrets. These are separate role planes and are frequently confused.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a vault with RBAC enabled
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az keyvault create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;vault-name&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;rg&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"eastus"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-rbac-authorization&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--enable-rbac-authorization true&lt;/code&gt; flag is important. Without it, the vault uses the legacy access policy model. Microsoft's current guidance is to use RBAC for all new vaults. The access policy model has a privilege escalation risk: any user with Contributor on the vault can modify access policies to grant themselves secret access. Under RBAC, only Owner and User Access Administrator can modify role assignments.&lt;/p&gt;

&lt;p&gt;Once the vault exists, add a secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az keyvault secret &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vault-name&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;vault-name&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"ApiKey"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--value&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;your-secret&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Local development
&lt;/h3&gt;

&lt;p&gt;Key Vault references are a portal-level feature. They do not apply locally. For local development, put the actual secret values directly in &lt;code&gt;local.settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ApiKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev-key-here"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app code does not change. The same &lt;code&gt;configuration["ApiKey"]&lt;/code&gt; call works locally (reading from the environment variable injected by Core Tools) and in Azure (reading from the Key Vault reference resolved by the platform).&lt;/p&gt;

&lt;p&gt;If your team needs local access to the actual Key Vault for testing, use &lt;code&gt;DefaultAzureCredential&lt;/code&gt; in your service registration and run &lt;code&gt;az login&lt;/code&gt; with an account that has Key Vault Secrets User on the vault. The credential chain tries Azure CLI authentication, so the logged-in developer account gets used automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Strongly-typed configuration with the options pattern
&lt;/h2&gt;

&lt;p&gt;Reading configuration with &lt;code&gt;configuration["MyKey"]&lt;/code&gt; works but gives you a stringly-typed API with no validation and no structure. The options pattern solves this by binding a configuration section to a typed class.&lt;/p&gt;

&lt;p&gt;Define the options class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApiOptions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Required&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;BaseUrl&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;TimeoutSeconds&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register and validate it in &lt;code&gt;Program.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ApiOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Api"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateDataAnnotations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateOnStart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;BindConfiguration("Api")&lt;/code&gt; binds from the &lt;code&gt;Api&lt;/code&gt; section of &lt;code&gt;IConfiguration&lt;/code&gt;. After GetSection strips the prefix, &lt;code&gt;ApiOptions.BaseUrl&lt;/code&gt; maps to the &lt;code&gt;Api:BaseUrl&lt;/code&gt; key, which in turn comes from the &lt;code&gt;Api__BaseUrl&lt;/code&gt; environment variable. The same property name works whether the setting originates from &lt;code&gt;appsettings.json&lt;/code&gt;, &lt;code&gt;local.settings.json&lt;/code&gt;, or an Azure App Setting.&lt;/p&gt;

&lt;p&gt;Inject the options into a function class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProcessOrderFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ApiOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ApiOptions&lt;/span&gt; &lt;span class="n"&gt;_api&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProcessOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthorizationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// _api.BaseUrl and _api.TimeoutSeconds are validated and available&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeoutSeconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/orders"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OkResult&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ValidateOnStart
&lt;/h3&gt;

&lt;p&gt;Without &lt;code&gt;ValidateOnStart()&lt;/code&gt;, validation fires when &lt;code&gt;.Value&lt;/code&gt; is first accessed. A misconfigured but rarely-invoked function can pass through a cold start and fail only at runtime, when you least expect it. With &lt;code&gt;ValidateOnStart()&lt;/code&gt;, the host throws &lt;code&gt;OptionsValidationException&lt;/code&gt; during startup and refuses to run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Microsoft.Extensions.Options.OptionsValidationException:
  DataAnnotation validation failed for 'ApiOptions' members:
    'BaseUrl' with the error: 'The BaseUrl field is required.'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This converts a silent runtime failure into an explicit startup failure, which is much easier to diagnose.&lt;/p&gt;

&lt;h3&gt;
  
  
  IOptions vs IOptionsSnapshot vs IOptionsMonitor
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;IOptions&amp;lt;T&amp;gt;&lt;/code&gt; in Azure Functions. The value is cached per singleton lifetime, which is correct: App Settings do not change at runtime without a restart, and a restart rebuilds the singleton anyway.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IOptionsSnapshot&amp;lt;T&amp;gt;&lt;/code&gt; is Scoped. Injecting it into a singleton throws at runtime. Functions does not have the same clear scope-per-request lifecycle as ASP.NET Core, so &lt;code&gt;IOptionsSnapshot&amp;lt;T&amp;gt;&lt;/code&gt; causes subtle failures.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IOptionsMonitor&amp;lt;T&amp;gt;&lt;/code&gt; is safe in singletons and works if you need to respond to live configuration changes (for example, from Azure App Configuration with a refresh sentinel). For standard App Settings, it is more complexity than the scenario requires.&lt;/p&gt;

&lt;h3&gt;
  
  
  local.settings.json for options
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Values&lt;/code&gt; section is a flat dictionary. To represent the &lt;code&gt;Api&lt;/code&gt; section locally, use the &lt;code&gt;__&lt;/code&gt; separator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"IsEncrypted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UseDevelopmentStorage=true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"FUNCTIONS_WORKER_RUNTIME"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotnet-isolated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Api__BaseUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Api__TimeoutSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Azure, create App Settings with the same names: &lt;code&gt;Api__BaseUrl&lt;/code&gt; and &lt;code&gt;Api__TimeoutSeconds&lt;/code&gt;. The binding is identical in both environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three-layer mental model
&lt;/h2&gt;

&lt;p&gt;When a configuration problem comes up, the question to ask is: which process needs this value?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trigger and binding connections&lt;/strong&gt; are resolved by the host process. They must be environment variables: either a &lt;code&gt;Values&lt;/code&gt; entry in &lt;code&gt;local.settings.json&lt;/code&gt;, an Azure App Setting, or a Key Vault reference on an App Setting. No other configuration source reaches the host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Application settings&lt;/strong&gt; (anything your code reads via &lt;code&gt;IConfiguration&lt;/code&gt;) come from the full configuration pipeline: environment variables, &lt;code&gt;appsettings.json&lt;/code&gt;, &lt;code&gt;appsettings.{Environment}.json&lt;/code&gt;, and any sources you add in &lt;code&gt;Program.cs&lt;/code&gt;. Because environment variables are part of this pipeline, all App Settings are also available in &lt;code&gt;IConfiguration&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets&lt;/strong&gt; live in Key Vault and are surfaced as App Settings via the reference syntax. Your code sees them as ordinary environment variables and reads them through &lt;code&gt;IConfiguration&lt;/code&gt; like any other setting.&lt;/p&gt;

&lt;p&gt;The full working &lt;code&gt;ConfigurationDemo&lt;/code&gt; project — &lt;code&gt;ApiOptions&lt;/code&gt;, registration in &lt;code&gt;Program.cs&lt;/code&gt;, and &lt;code&gt;ProcessOrderFunction&lt;/code&gt; — is in the &lt;a href="https://github.com/MO2k4/azure-functions-samples" rel="noopener noreferrer"&gt;azure-functions-samples&lt;/a&gt; repository. Clone it, copy &lt;code&gt;local.settings.json.example&lt;/code&gt; to &lt;code&gt;local.settings.json&lt;/code&gt;, and run &lt;code&gt;func start&lt;/code&gt; to see &lt;code&gt;ValidateOnStart&lt;/code&gt; in action.&lt;/p&gt;




&lt;p&gt;What do you use to manage secrets in your Azure Functions projects: Key Vault references, Azure App Configuration, or something else? Drop it in the comments.&lt;/p&gt;

&lt;h1&gt;
  
  
  AzureFunctions #DotNet #Azure #Security #KeyVault
&lt;/h1&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 6: Configuration Done Right: Settings, Secrets, and Key Vault (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>dotnet</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Understanding the Isolated Worker Model</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 06 Mar 2026 07:21:24 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4</link>
      <guid>https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4</guid>
      <description>&lt;h2&gt;
  
  
  The problem the isolated model solves
&lt;/h2&gt;

&lt;p&gt;You add a NuGet reference to &lt;code&gt;Newtonsoft.Json&lt;/code&gt; 13.0. Your code compiles. Your unit tests pass. You deploy to Azure Functions, and at runtime, your function silently uses version 12.0.3.&lt;/p&gt;

&lt;p&gt;No error. No warning. Just the host's copy of the assembly winning the load, because your function code and the Azure Functions runtime shared a single process.&lt;/p&gt;

&lt;p&gt;This was the &lt;strong&gt;in-process model&lt;/strong&gt;, and for years it was the only way to build .NET Azure Functions. Your function assemblies loaded directly into the same CLR instance as the Functions host. The host pinned its own versions of core packages: &lt;code&gt;Newtonsoft.Json&lt;/code&gt;, &lt;code&gt;Microsoft.Extensions.DependencyInjection&lt;/code&gt;, ASP.NET Core libraries, and dozens of others. If your code depended on a different version, the host's version won at load time. You had no way to override it.&lt;/p&gt;

&lt;p&gt;The version conflict problem went beyond JSON serialization. The host determined which .NET runtime your code ran on. When .NET 7 shipped, you could not target it until the Functions team updated the host. When .NET 8 arrived, the same waiting game. Your application's target framework was not your decision; it was the host's.&lt;/p&gt;

&lt;p&gt;Startup control was equally limited. The in-process model offered &lt;strong&gt;&lt;code&gt;FunctionsStartup&lt;/code&gt;&lt;/strong&gt; as an extension point for dependency injection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;assembly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;FunctionsStartup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MyStartup&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyStartup&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FunctionsStartup&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IFunctionsHostBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOrderService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gave you a DI container, but nothing else. No middleware pipeline. No request/response interception. No control over serialization settings, logging providers, or configuration sources beyond what the host exposed. If you wanted to add authentication middleware, or swap the JSON serializer for &lt;code&gt;System.Text.Json&lt;/code&gt;, or wire up OpenTelemetry tracing, you were working against the grain.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FunctionsStartup&lt;/code&gt; was a workaround bolted onto a hosting model that was never designed for extensibility. The isolated worker model replaced it entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two processes, one function app
&lt;/h2&gt;

&lt;p&gt;The isolated model splits your function app into two separate OS processes. The &lt;strong&gt;Azure Functions host&lt;/strong&gt; (&lt;code&gt;func.exe&lt;/code&gt;) handles triggers, bindings, and routing. Your code runs in a separate &lt;strong&gt;worker process&lt;/strong&gt; (&lt;code&gt;dotnet.exe&lt;/code&gt;) with its own CLR, its own assembly loader, and its own dependency graph.&lt;/p&gt;

&lt;p&gt;A single environment variable controls this split. Setting &lt;strong&gt;&lt;code&gt;FUNCTIONS_WORKER_RUNTIME&lt;/code&gt;&lt;/strong&gt; to &lt;code&gt;dotnet-isolated&lt;/code&gt; tells the host to spawn a worker process instead of loading your assemblies directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Azure Functions Host (func.exe)
  ├── Trigger listeners (HTTP, Service Bus, Event Hub...)
  ├── Binding infrastructure
  ├── host.json configuration
  └── gRPC server
         ↕ gRPC / Protobuf over HTTP/2
Worker Process (dotnet.exe / your app)
  ├── Program.cs bootstrap
  ├── Your DI container
  ├── Your middleware pipeline
  └── Your function code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two processes communicate over &lt;strong&gt;gRPC&lt;/strong&gt; using Protocol Buffers serialized over HTTP/2. When a trigger fires (an HTTP request arrives, a Service Bus message lands), the host serializes the trigger data into a Protobuf message and sends it to the worker. The worker executes your function, serializes the result, and sends it back.&lt;/p&gt;

&lt;p&gt;These C4 diagrams show the two-process architecture in more detail:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fydxm4u1elbmtgyc5xnbt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fydxm4u1elbmtgyc5xnbt.png" alt="C4 container diagram showing the isolated worker architecture" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxu1a8b5ojkck9rz0g0g7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxu1a8b5ojkck9rz0g0g7.png" alt="C4 dynamic diagram showing the request flow between host and worker" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;Program.cs&lt;/code&gt; sets up the gRPC client, DI container, and middleware pipeline in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsWebApplication&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ExceptionHandlingMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CorrelationIdMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddApplicationInsightsTelemetryWorkerService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsApplicationInsights&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOrderService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ConfigureFunctionsWebApplication()&lt;/code&gt; registers the gRPC client that talks back to the host, enables ASP.NET Core integration for HTTP triggers, and gives you the middleware pipeline shown above. If you do not need HTTP trigger support, &lt;code&gt;ConfigureFunctionsWorkerDefaults()&lt;/code&gt; does the same setup without the ASP.NET Core integration.&lt;/p&gt;

&lt;p&gt;Because each process loads its own assemblies independently, the version conflict problem disappears. Your worker targets .NET 10 and references &lt;code&gt;Newtonsoft.Json&lt;/code&gt; 13.0.3? That is what runs. The host still uses whatever versions it needs internally, and the two never collide.&lt;/p&gt;

&lt;p&gt;The trade-off is that every function invocation crosses a process boundary. The host serializes trigger data, sends it over gRPC, and the worker deserializes it. On the same machine, the latency cost is negligible for most workloads. Where you will notice it is &lt;strong&gt;cold starts&lt;/strong&gt;: the runtime now needs to spin up two processes instead of one. For high-throughput, latency-sensitive functions that fire thousands of times per second, measure the overhead in your specific scenario. For the vast majority of production workloads (processing orders, handling webhooks, running scheduled cleanup jobs), the isolation is worth far more than the milliseconds it costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you gain
&lt;/h2&gt;

&lt;p&gt;The isolated worker model removes real constraints that made the in-process model frustrating in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  No more assembly conflicts
&lt;/h3&gt;

&lt;p&gt;Your worker runs in its own process with its own dependency graph. The host loads whatever versions it needs; your application loads whatever versions you reference. Two processes, two sets of assemblies, zero conflicts. The Newtonsoft.Json problem from the opening of this article cannot happen in the isolated model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full startup control via Program.cs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsWebApplication&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOrderService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddApplicationInsightsTelemetryWorkerService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsApplicationInsights&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This replaces the &lt;code&gt;[assembly: FunctionsStartup(typeof(MyStartup))]&lt;/code&gt; attribute and the &lt;code&gt;Startup&lt;/code&gt; class you had to wire up in the in-process model. The whole application now bootstraps through the &lt;strong&gt;.NET Generic Host&lt;/strong&gt;, the same pattern you already know from ASP.NET Core and worker services.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FunctionsApplication.CreateBuilder(args)&lt;/code&gt; sets up the host builder with Functions-specific defaults. &lt;code&gt;ConfigureFunctionsWebApplication()&lt;/code&gt; enables ASP.NET Core integration so your HTTP triggers can work with &lt;code&gt;HttpRequest&lt;/code&gt; and &lt;code&gt;HttpResponse&lt;/code&gt; directly instead of the SDK's custom types.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Services&lt;/code&gt; block is standard dependency injection. &lt;code&gt;AddSingleton&amp;lt;IOrderService, OrderService&amp;gt;()&lt;/code&gt; registers your own service. &lt;code&gt;AddApplicationInsightsTelemetryWorkerService()&lt;/code&gt; and &lt;code&gt;ConfigureFunctionsApplicationInsights()&lt;/code&gt; wire up telemetry for the worker process (both are needed: the first adds the base SDK, the second configures Functions-specific log filtering).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;builder.Build().Run()&lt;/code&gt; starts the worker and connects it to the host over gRPC. If you have written a .NET 8 web API, this code should look familiar, because it is the same hosting model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Middleware pipeline
&lt;/h3&gt;

&lt;p&gt;The in-process model had no middleware. If you needed cross-cutting behavior (logging correlation IDs, catching unhandled exceptions, validating tokens) you were stuck wiring it through WebJobs SDK extension points or scattering try/catch blocks across every function.&lt;/p&gt;

&lt;p&gt;The isolated model gives you an ASP.NET Core-style pipeline around every function invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CorrelationIdMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ExceptionHandlingMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each middleware runs in order before the function executes, then unwinds in reverse order after. You build one by implementing &lt;strong&gt;&lt;code&gt;IFunctionsWorkerMiddleware&lt;/code&gt;&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CorrelationIdMiddleware&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IFunctionsWorkerMiddleware&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FunctionContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FunctionExecutionDelegate&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Runs before the function&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;correlationId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Runs after the function&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FunctionContext&lt;/code&gt; gives you access to the invocation metadata, bindings, and the &lt;code&gt;IServiceProvider&lt;/code&gt;. The &lt;code&gt;next&lt;/code&gt; delegate calls either the next middleware in the chain or the function itself. Everything before &lt;code&gt;await next(context)&lt;/code&gt; runs on the way in; everything after runs on the way out.&lt;/p&gt;

&lt;p&gt;In production, you would typically read an incoming correlation ID from a header or message property, fall back to generating one if it is missing, then stash it in &lt;code&gt;context.Items&lt;/code&gt; so the function and downstream services can pick it up. Exception-handling middleware wraps the &lt;code&gt;next&lt;/code&gt; call in a try/catch, logs the failure with structured context, and returns a consistent error response instead of letting the host surface a generic 500.&lt;/p&gt;

&lt;h3&gt;
  
  
  .NET version flexibility
&lt;/h3&gt;

&lt;p&gt;The worker process runs whatever .NET version you target, independently of the host. The host stays on its own runtime; your code stays on yours.&lt;/p&gt;

&lt;p&gt;Today, the isolated model supports .NET 8, .NET 9, .NET 10, and even .NET Framework 4.8 (for teams that cannot migrate legacy libraries). The in-process model was capped at .NET 8 with &lt;code&gt;v4&lt;/code&gt; of the Functions runtime and will never support .NET 9 or later. When .NET 12 ships, you will update your &lt;code&gt;TargetFramework&lt;/code&gt;, redeploy, and move on. No waiting for the Azure Functions team to update the host.&lt;/p&gt;

&lt;p&gt;Your release cycle and the host's release cycle are decoupled. You upgrade on your schedule.&lt;/p&gt;




&lt;h2&gt;
  
  
  What changes from in-process
&lt;/h2&gt;

&lt;p&gt;Most of the migration is mechanical. The conceptual model shifts, but the actual code changes follow a predictable pattern. Once you have seen each one, you can work through a real codebase systematically.&lt;/p&gt;

&lt;h3&gt;
  
  
  The function attribute
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In-process&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProcessOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Isolated (with ASP.NET Core integration)&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProcessOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;[FunctionName]&lt;/code&gt; comes from the WebJobs SDK (&lt;code&gt;Microsoft.Azure.WebJobs&lt;/code&gt;). &lt;code&gt;[Function]&lt;/code&gt; comes from the Functions Worker SDK (&lt;code&gt;Microsoft.Azure.Functions.Worker&lt;/code&gt;). The attribute names differ by one word, which makes a global search-and-replace dangerous: you need to update the package reference and the attribute name together, or you will reference an attribute that does not exist in your new dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP types
&lt;/h3&gt;

&lt;p&gt;The isolated model gives you two ways to handle HTTP triggers, and the difference matters:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Types used&lt;/th&gt;
&lt;th&gt;When to choose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ASP.NET Core integration&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;HttpRequest&lt;/code&gt; / &lt;code&gt;IActionResult&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;New projects, or migrating from in-process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in model&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;HttpRequestData&lt;/code&gt; / &lt;code&gt;HttpResponseData&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Legacy compatibility only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;ASP.NET Core integration is the path to take. It means your HTTP functions look exactly like ASP.NET Core controller actions, and all the routing, model binding, and result types you already know apply. It requires two things: the &lt;code&gt;Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore&lt;/code&gt; package, and &lt;code&gt;ConfigureFunctionsWebApplication()&lt;/code&gt; in &lt;code&gt;Program.cs&lt;/code&gt; instead of &lt;code&gt;ConfigureFunctionsWorkerDefaults()&lt;/code&gt;. If you find tutorials using &lt;code&gt;HttpRequestData&lt;/code&gt;, they predate the ASP.NET Core integration and are using the older built-in types. You can use either, but the ASP.NET Core path removes an entire class of "why does this work differently than my API controllers?" questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Output bindings
&lt;/h3&gt;

&lt;p&gt;In-process used &lt;code&gt;out&lt;/code&gt; parameters for output bindings. Isolated uses return values with attributes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In-process: out parameters&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;outputMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Isolated: return value with output binding attribute&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProcessOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;QueueOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders-processed"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;QueueTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders-pending"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;processedMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;out&lt;/code&gt; parameter approach was a side effect of how the WebJobs SDK wired up bindings. In isolated, bindings are attributes on the return type, which makes the data flow explicit: what the function returns is what gets written to the binding.&lt;/p&gt;

&lt;p&gt;When you need multiple outputs (for example, writing to a queue and returning an HTTP response), you define a dedicated return type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;MultiOutputResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;QueueOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dead-letter"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;DeadLetterMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each property carries its own binding attribute. The runtime inspects the returned record and routes each value to the appropriate destination. This is more verbose than &lt;code&gt;out&lt;/code&gt; parameters for simple cases, but it makes multi-output functions far easier to read: every output is explicit in the return type definition rather than scattered across a function signature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency injection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In-process: FunctionsStartup + IFunctionsHostBuilder&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;assembly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;FunctionsStartup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Startup&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Startup&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FunctionsStartup&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IFunctionsHostBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IMyService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MyService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Isolated: Program.cs (standard Generic Host)&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IMyService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MyService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FunctionsStartup&lt;/code&gt; was a Functions-specific extension point built on top of the Generic Host. In isolated, there is no extension point: your function app &lt;em&gt;is&lt;/em&gt; a Generic Host application. &lt;code&gt;Program.cs&lt;/code&gt; is the entry point, and the &lt;code&gt;Services&lt;/code&gt; property is a standard &lt;code&gt;IServiceCollection&lt;/code&gt;. Delete &lt;code&gt;Startup.cs&lt;/code&gt;, delete the &lt;code&gt;Microsoft.Azure.Functions.Extensions&lt;/code&gt; package reference, and move your service registrations into &lt;code&gt;Program.cs&lt;/code&gt;. There is nothing Functions-specific about how DI works after that.&lt;/p&gt;

&lt;h3&gt;
  
  
  ILogger injection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In-process: ILogger passed as function parameter&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Isolated: inject ILogger&amp;lt;T&amp;gt; via constructor&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProcessOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Processing order"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the in-process model, the runtime injected &lt;code&gt;ILogger&lt;/code&gt; directly into the function method as a parameter. That was a WebJobs SDK feature with no equivalent in the isolated model. In isolated, your function class is an ordinary class that the DI container constructs. You inject &lt;code&gt;ILogger&amp;lt;T&amp;gt;&lt;/code&gt; through the constructor, exactly as you would in any .NET service. The generic type parameter means your logs are automatically scoped to the class name in Application Insights.&lt;/p&gt;

&lt;p&gt;Every function that currently takes &lt;code&gt;ILogger log&lt;/code&gt; as a method parameter needs to become an instance class with a constructor. That is one of the more time-consuming parts of migration for large codebases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package references and project type
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- In-process --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;OutputType&amp;gt;&lt;/span&gt;Library&lt;span class="nt"&gt;&amp;lt;/OutputType&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- implicit, often omitted --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.NET.Sdk.Functions"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"4.x"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Isolated: a real executable --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;OutputType&amp;gt;&lt;/span&gt;Exe&lt;span class="nt"&gt;&amp;lt;/OutputType&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;FrameworkReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.AspNetCore.App"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.21.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.Sdk"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.17.2"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.2.1"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.ApplicationInsights.WorkerService"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.22.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Azure.Functions.Worker.ApplicationInsights"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.2.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;OutputType&amp;gt;Exe&amp;lt;/OutputType&amp;gt;&lt;/code&gt; is not optional. The isolated worker is a process that starts, connects to the host over gRPC, and runs until it is shut down. It is not a library that gets loaded into another process. The old &lt;code&gt;Microsoft.NET.Sdk.Functions&lt;/code&gt; meta-package is replaced by separate packages: the core worker, the build SDK (which handles source generation for bindings), the HTTP ASP.NET Core extension, and two Application Insights packages.&lt;/p&gt;

&lt;p&gt;Your binding extensions also change package namespace. Every &lt;code&gt;Microsoft.Azure.WebJobs.Extensions.*&lt;/code&gt; package becomes a &lt;code&gt;Microsoft.Azure.Functions.Worker.Extensions.*&lt;/code&gt; equivalent. The NuGet package names differ; the binding attributes inside them often keep the same names, which reduces the code changes needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Static classes become instance classes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In-process: static class (common pattern)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderFunction&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProcessOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Isolated: instance class required for constructor injection&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderFunction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProcessOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Static function classes were idiomatic in in-process because the runtime called your method directly by reflection and supplied everything through parameters. Constructor injection is impossible on a static class, so isolated requires instance classes. The compiler will not tell you this immediately: your static class will compile fine, but the runtime will fail to instantiate it because it cannot inject dependencies into a static constructor. Make the class non-static and add a constructor for your dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON serialization
&lt;/h3&gt;

&lt;p&gt;In-process defaulted to Newtonsoft.Json for binding serialization. Isolated defaults to &lt;code&gt;System.Text.Json&lt;/code&gt;. This is the change most likely to produce silent runtime bugs rather than compilation errors.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[JsonProperty("field_name")]&lt;/code&gt; does not exist in &lt;code&gt;System.Text.Json&lt;/code&gt;. The equivalent is &lt;code&gt;[JsonPropertyName("field_name")]&lt;/code&gt;. CamelCase naming defaults differ between the two libraries. Null handling, reference loop handling, and enum serialization all differ. If your functions receive JSON payloads, serialize objects to queues, or return JSON from HTTP triggers, test each one end-to-end after migration. A mismatch between what your function now serializes and what downstream consumers expect will not show up at compile time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Imperative bindings
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;IBinder&lt;/code&gt;, the in-process mechanism for creating bindings at runtime (for example, writing to a blob whose path you only know after reading a message), has no equivalent in isolated. The recommended replacement is injecting the Azure SDK client directly: &lt;code&gt;BlobServiceClient&lt;/code&gt;, &lt;code&gt;QueueClient&lt;/code&gt;, &lt;code&gt;ServiceBusClient&lt;/code&gt;. This is cleaner code in either model: SDK clients are testable, type-safe, and do not require the Functions binding infrastructure to work.&lt;/p&gt;




&lt;h2&gt;
  
  
  The .NET 10 requirement
&lt;/h2&gt;

&lt;p&gt;.NET 10 only supports the isolated model. There is no in-process support for .NET 10, and none is planned. If you are starting a new project today and targeting .NET 10, you are already using isolated by necessity; this article just explains the architecture behind it.&lt;/p&gt;

&lt;p&gt;The support matrix makes the direction clear:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;.NET version&lt;/th&gt;
&lt;th&gt;In-process&lt;/th&gt;
&lt;th&gt;Isolated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET 8 (LTS)&lt;/td&gt;
&lt;td&gt;Yes (final version)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET 9 (STS)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET 10 (LTS)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;.NET 8 is the last version the in-process model will ever support. If your function app runs on .NET 8 today, you are already at the ceiling. Staying on in-process means staying on .NET 8 until November 2026, then losing support entirely. Migrating to isolated means you can move to .NET 10 now, get the latest runtime improvements, and upgrade again when .NET 12 arrives without any coordination with the Azure Functions team.&lt;/p&gt;




&lt;h2&gt;
  
  
  The November 2026 deadline
&lt;/h2&gt;

&lt;p&gt;In-process model support ends on November 10, 2026. That date is not arbitrary: it aligns with the end-of-life date for .NET 8 LTS. After that point, in-process function apps receive no security patches, no bug fixes, and no platform updates. The deadline is firm.&lt;/p&gt;

&lt;p&gt;Start your inventory now. This PowerShell script identifies every in-process function app in your subscription:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$FunctionApps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-AzFunctionApp&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$App&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$FunctionApps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Runtime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'dotnet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Write-Output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; - in-process, needs migration"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;Runtime&lt;/code&gt; value of &lt;code&gt;dotnet&lt;/code&gt; means in-process. &lt;code&gt;dotnet-isolated&lt;/code&gt; means already migrated. Run this across every subscription where you might have deployed function apps, including non-production environments where older versions sometimes linger.&lt;/p&gt;

&lt;p&gt;The migration itself is mechanical for most functions: update packages, add &lt;code&gt;Program.cs&lt;/code&gt;, fix compilation errors, update attributes. The problem is not the mechanical work; it is the edge cases that surface during testing. A function that uses &lt;code&gt;IBinder&lt;/code&gt;, a binding attribute with changed properties, a JSON payload that now serializes differently: each one is a small investigation. In a codebase with dozens of functions, those investigations add up.&lt;/p&gt;

&lt;p&gt;The recommended order of attack:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the script above and produce a full inventory with function counts per app.&lt;/li&gt;
&lt;li&gt;Start with the simplest apps: HTTP triggers, no output bindings, no &lt;code&gt;IBinder&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Update packages and &lt;code&gt;Program.cs&lt;/code&gt; first, then fix compilation errors function by function.&lt;/li&gt;
&lt;li&gt;Test locally with &lt;code&gt;func start&lt;/code&gt; before touching Azure.&lt;/li&gt;
&lt;li&gt;Deploy to a staging slot and run your smoke tests before swapping to production.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The staging slot step matters more here than in typical deployments. When you swap, the &lt;code&gt;FUNCTIONS_WORKER_RUNTIME&lt;/code&gt; app setting switches from &lt;code&gt;dotnet&lt;/code&gt; to &lt;code&gt;dotnet-isolated&lt;/code&gt;. If your code and the app setting get out of sync even briefly, the app enters an error state. Slots let you validate the new configuration in production infrastructure before making it live.&lt;/p&gt;

&lt;p&gt;Waiting until Q3 2026 to start leaves no room for the edge cases. Start with your least critical app now, learn the migration pattern in a low-stakes context, and work forward from there.&lt;/p&gt;




&lt;h2&gt;
  
  
  One more thing: Flex Consumption is isolated-only
&lt;/h2&gt;

&lt;p&gt;The Flex Consumption plan is Microsoft's newest Azure Functions hosting option. It scales each function independently rather than scaling the whole app, supports always-ready instances that eliminate cold starts for your busiest functions, and bills at a finer granularity than the standard Consumption plan. If any of that sounds appealing, the isolated worker model is a prerequisite.&lt;/p&gt;

&lt;p&gt;In-process cannot run on Flex Consumption at all. The two are architecturally incompatible: Flex Consumption requires the worker process model to manage per-function scaling, and in-process has no worker process to manage.&lt;/p&gt;

&lt;p&gt;If you are evaluating hosting options for a new function app, that decision is already made for you: Flex Consumption is isolated-only, and isolated is where all future platform investment is going. Starting on in-process today means either migrating before you can move to Flex Consumption, or accepting a hosting model that cannot take advantage of the newest platform capabilities.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuration: two surfaces, not one
&lt;/h2&gt;

&lt;p&gt;After migration, one category of bug appears repeatedly: a developer configures something in &lt;code&gt;Program.cs&lt;/code&gt; and it has no effect on trigger behavior. The root cause is always the same: the isolated model has two separate configuration surfaces, one for the host process and one for the worker process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Host configuration&lt;/strong&gt; governs triggers, bindings, and scaling. Two sources feed it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;host.json&lt;/code&gt;: extension settings, retry policies, connection concurrency, scale behavior.&lt;/li&gt;
&lt;li&gt;Environment variables and Azure App Service application settings: connection strings that the host uses to connect to Service Bus, Storage, Event Hub, and other binding sources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Worker configuration&lt;/strong&gt; governs your application code. Two sources feed it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;appsettings.json&lt;/code&gt;: loaded automatically by &lt;code&gt;FunctionsApplication.CreateBuilder()&lt;/code&gt;, accessible through &lt;code&gt;IConfiguration&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Anything you wire up in &lt;code&gt;Program.cs&lt;/code&gt;: additional configuration providers, secrets, feature flags.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The critical rule: connection strings for bindings go in environment variables (or App Service application settings in production), not in &lt;code&gt;appsettings.json&lt;/code&gt;. The host process that initializes the Service Bus trigger listener or the Storage queue poller reads from environment variables. It cannot read your worker's &lt;code&gt;appsettings.json&lt;/code&gt;. A connection string that lives only in &lt;code&gt;appsettings.json&lt;/code&gt; will work fine for any code in your worker that reads it directly (for example, an Azure SDK client you construct manually) but will cause the binding itself to fail at startup with a cryptic "missing connection string" error.&lt;/p&gt;

&lt;p&gt;Locally, &lt;code&gt;local.settings.json&lt;/code&gt; maps its &lt;code&gt;Values&lt;/code&gt; section into environment variables when the Functions host starts, which is why everything works in local development even when you have not thought carefully about this split. In Azure, you configure application settings in the portal or via deployment scripts, and they become environment variables for both processes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration gotchas worth knowing in advance
&lt;/h2&gt;

&lt;p&gt;The following issues are not obvious from the migration documentation and tend to surface late, when you are integrating and testing rather than making mechanical code changes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;ILogger&lt;/code&gt; as a method parameter is gone.&lt;/strong&gt; The runtime no longer injects it. Every function that currently takes &lt;code&gt;ILogger log&lt;/code&gt; as a parameter needs to become an instance class with a constructor that accepts &lt;code&gt;ILogger&amp;lt;T&amp;gt;&lt;/code&gt;. In large codebases, this touches many files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;JSON serialization changes silently.&lt;/strong&gt; Moving from Newtonsoft.Json to &lt;code&gt;System.Text.Json&lt;/code&gt; changes how your bindings serialize and deserialize data. &lt;code&gt;[JsonProperty]&lt;/code&gt; becomes &lt;code&gt;[JsonPropertyName]&lt;/code&gt;. Null values, camelCase defaults, and enum handling all differ. A function that processes messages from a queue may silently start deserializing them incorrectly if the attribute names change. Test every binding that touches JSON.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Application Insights log filtering moves from &lt;code&gt;host.json&lt;/code&gt; to &lt;code&gt;Program.cs&lt;/code&gt;.&lt;/strong&gt; Log levels configured under &lt;code&gt;logging.logLevel&lt;/code&gt; in &lt;code&gt;host.json&lt;/code&gt; no longer apply to code running in the worker process. To filter worker logs, call &lt;code&gt;ConfigureFunctionsApplicationInsights()&lt;/code&gt; in &lt;code&gt;Program.cs&lt;/code&gt; and configure the log level there. Without this, you may find your worker logs missing from Application Insights, or flooded with debug output you expected to filter out.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;IAsyncCollector&amp;lt;T&amp;gt;&lt;/code&gt; has no direct equivalent.&lt;/strong&gt; If your functions write multiple messages to a queue or table using &lt;code&gt;IAsyncCollector&amp;lt;T&amp;gt;&lt;/code&gt;, replace it with an array property on a dedicated return type. &lt;code&gt;IAsyncCollector&amp;lt;string&amp;gt;&lt;/code&gt; becomes &lt;code&gt;string[]&lt;/code&gt; on a record that your function returns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Blob binding attribute properties changed.&lt;/strong&gt; &lt;code&gt;[Blob("container/path", FileAccess.Write)]&lt;/code&gt; does not exist in the isolated extension. The equivalent is &lt;code&gt;[BlobOutput("container/path")]&lt;/code&gt;. The &lt;code&gt;FileAccess&lt;/code&gt; enum property was removed; the direction is now expressed by whether you use &lt;code&gt;[BlobInput]&lt;/code&gt; or &lt;code&gt;[BlobOutput]&lt;/code&gt;. This is a compilation error, which means you will catch it, but expect to update every blob binding attribute.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom configuration in &lt;code&gt;Program.cs&lt;/code&gt; is invisible to the host.&lt;/strong&gt; If you read a connection string from &lt;code&gt;appsettings.json&lt;/code&gt; in &lt;code&gt;Program.cs&lt;/code&gt; and wire it up to a service, that configuration does not flow to the binding runtime. Trigger connections must come from environment variables. This is a duplicate of the two-surfaces rule above, but it is worth restating because it manifests as a confusing runtime error rather than a compilation failure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;FUNCTIONS_WORKER_RUNTIME&lt;/code&gt; and the deployed code must change together.&lt;/strong&gt; If the app setting in Azure says &lt;code&gt;dotnet&lt;/code&gt; but you deploy isolated code (or the reverse), the function app enters an error state on startup. Use deployment slots to change the app setting and deploy the code atomically, then validate in staging before swapping to production.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;The isolated model is the foundation everything else in this series sits on. The HTTP trigger patterns in Part 2 and the timer, queue, and blob triggers in Part 3 all assumed isolated; now you know why those patterns look the way they do. The local development setup in Part 4 works as it does because the worker process can be debugged independently of the host. The architecture is not incidental; it shapes every practical detail.&lt;/p&gt;

&lt;p&gt;If you are migrating an existing in-process app, the &lt;a href="https://learn.microsoft.com/azure/azure-functions/migrate-dotnet-to-isolated-model" rel="noopener noreferrer"&gt;Microsoft migration guide&lt;/a&gt; walks through the steps with tooling support including a migration tool that handles some of the mechanical changes automatically. Use it as a checklist, but read through the sections on JSON serialization and Application Insights filtering before you declare the migration done: those two areas produce the most post-migration bugs.&lt;/p&gt;

&lt;p&gt;If you are starting a new project, start on isolated and .NET 10. The in-process model has no future, and building on it today means doing this migration under deadline pressure later.&lt;/p&gt;

&lt;p&gt;Which part of the migration gave you the most trouble, or are you starting fresh with isolated from day one?&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: &lt;a href="https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925"&gt;Local Development Setup: Tools, Debugging, and Hot Reload&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 5: Understanding the Isolated Worker Model (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>serverless</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Local Development Setup: Tools, Debugging, and Hot Reload</title>
      <dc:creator>Martin Oehlert</dc:creator>
      <pubDate>Fri, 27 Feb 2026 06:32:38 +0000</pubDate>
      <link>https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925</link>
      <guid>https://dev.to/martin_oehlert/local-development-setup-tools-debugging-and-hot-reload-2925</guid>
      <description>&lt;p&gt;You add a breakpoint. You press F5. The function executes. The breakpoint never fires.&lt;/p&gt;

&lt;p&gt;This is the most common introduction to Azure Functions local development. The reason is non-obvious: when you debug a .NET isolated worker function, two separate processes run. Your debugger attached to the host process (&lt;code&gt;func.exe&lt;/code&gt;) instead of the worker process (&lt;code&gt;dotnet.exe&lt;/code&gt;), which is where your code actually executes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;local.settings.json&lt;/code&gt; causes a different kind of confusion. Unlike &lt;code&gt;appsettings.json&lt;/code&gt;, it injects environment variables: put a connection string in the wrong section and your bindings silently break. Azurite is a related trap: timer, queue, and blob triggers all use Azure Storage internally, so the emulator has to be running before those trigger types will initialize, even if your function code touches no storage directly.&lt;/p&gt;

&lt;p&gt;This covers the full local setup for Azure Functions with .NET 10 and the isolated worker model. What's not here: deployment, Application Insights (that's Part 9), or unit testing (Part 7).&lt;/p&gt;




&lt;h2&gt;
  
  
  What you need: three installs
&lt;/h2&gt;

&lt;p&gt;Three things have to be installed and working before you can run any Azure Function locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A. .NET 10 SDK&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet &lt;span class="nt"&gt;--version&lt;/span&gt;   &lt;span class="c"&gt;# expect 10.x.x&lt;/span&gt;
dotnet &lt;span class="nt"&gt;--list-sdks&lt;/span&gt; &lt;span class="c"&gt;# confirm 10.x is listed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you followed Parts 1-3, this is already done. If not, download from &lt;a href="https://dotnet.microsoft.com/download" rel="noopener noreferrer"&gt;dot.net&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;B. Azure Functions Core Tools v4&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Core Tools provides the &lt;code&gt;func&lt;/code&gt; CLI that starts the local Functions host. Install it with your package manager:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt;: &lt;code&gt;brew tap azure/functions &amp;amp;&amp;amp; brew install azure-functions-core-tools@4&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: &lt;code&gt;winget install Microsoft.Azure.FunctionsCoreTools&lt;/code&gt; or the &lt;a href="https://github.com/Azure/azure-functions-core-tools/releases" rel="noopener noreferrer"&gt;MSI download&lt;/a&gt; (64-bit required for VS Code debugging)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt;: APT install from Microsoft package feeds (see &lt;a href="https://learn.microsoft.com/azure/azure-functions/functions-run-local" rel="noopener noreferrer"&gt;the official instructions&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm (cross-platform)&lt;/strong&gt;: &lt;code&gt;npm i -g azure-functions-core-tools@4 --unsafe-perm true&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A note on the npm option for Windows: if you are using &lt;code&gt;Microsoft.Azure.Functions.Worker.Sdk&lt;/code&gt; 2.x, which enables &lt;code&gt;dotnet run&lt;/code&gt; support, the npm Core Tools installation does not work correctly for that workflow. Use the winget or MSI install instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;func &lt;span class="nt"&gt;--version&lt;/span&gt;   &lt;span class="c"&gt;# expect 4.x.x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;C. Azurite&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Azurite emulates Azure Blob, Queue, and Table storage locally. Install it once; you will need it running for every non-HTTP function.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VS Code extension&lt;/strong&gt; (recommended for VS Code users): search "Azurite" in the Extensions panel, or install &lt;code&gt;Azurite.azurite&lt;/code&gt; directly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt;: &lt;code&gt;npm install -g azurite&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt;: &lt;code&gt;docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual Studio 2026&lt;/strong&gt;: Azurite is bundled and starts automatically when you press F5&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The old Azure Storage Emulator is deprecated. If you see references to it in older articles or documentation, ignore them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run this sequence to confirm everything is wired up correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet &lt;span class="nt"&gt;--version&lt;/span&gt;
func &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# Start Azurite: either the VS Code extension (Command Palette: "Azurite: Start") or `azurite` in a terminal&lt;/span&gt;
func init TestProj &lt;span class="nt"&gt;--worker-runtime&lt;/span&gt; dotnet-isolated &lt;span class="nt"&gt;--target-framework&lt;/span&gt; net10.0
&lt;span class="nb"&gt;cd &lt;/span&gt;TestProj
func start
&lt;span class="c"&gt;# Should print: "Host lock lease acquired" with no errors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see &lt;code&gt;SocketException: Unable to connect to the remote server&lt;/code&gt; instead of the lock lease message, Azurite is not running. Start it first, then retry &lt;code&gt;func start&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The two-process model
&lt;/h2&gt;

&lt;p&gt;This is the section most local dev tutorials skip, which is why "my breakpoint won't fire" is such a common question.&lt;/p&gt;

&lt;p&gt;In the isolated worker model, two separate processes run every time you start a debug session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────┐
│   func.exe  (the Functions host)         │
│   - Manages triggers                     │
│   - Handles bindings                     │
│   - Routes HTTP requests                 │
│   - Runs on port 7071                    │
└──────────────────────┬───────────────────┘
                       │ gRPC
                       │
┌──────────────────────┴───────────────────┐
│   dotnet.exe  (your worker)              │
│   - Runs your actual function code       │
│   - Gets invoked by the host via gRPC    │
│   - This is where breakpoints fire       │
└──────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;func.exe&lt;/code&gt; is the Functions runtime: it manages triggers, handles bindings, and routes incoming requests. &lt;code&gt;dotnet.exe&lt;/code&gt; is your application: it receives invocation requests from the host over gRPC and runs your actual function code. Breakpoints live in &lt;code&gt;dotnet.exe&lt;/code&gt;, not &lt;code&gt;func.exe&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This matters for debugging in two ways. First, your debugger must attach to the worker process (&lt;code&gt;dotnet.exe&lt;/code&gt;), not to &lt;code&gt;func.exe&lt;/code&gt;. This is why the &lt;code&gt;launch.json&lt;/code&gt; the Azure Functions extension generates uses &lt;code&gt;"request": "attach"&lt;/code&gt; rather than &lt;code&gt;"request": "launch"&lt;/code&gt;, and why the process ID is set to &lt;code&gt;${command:azureFunctions.pickProcess}&lt;/code&gt; instead of a static value. Second, the two processes produce separate log streams with separate configuration: &lt;code&gt;host.json&lt;/code&gt; controls what &lt;code&gt;func.exe&lt;/code&gt; logs, while your &lt;code&gt;ILogger&amp;lt;T&amp;gt;&lt;/code&gt; calls are configured on the worker side. Changing one does not affect the other.&lt;/p&gt;

&lt;p&gt;For comparison: the deprecated in-process model runs everything in a single process. One process, one debugger attach target. That simplicity is gone with the isolated model, but the isolation is what allows it to run on .NET 10 instead of being locked to .NET 8.&lt;/p&gt;




&lt;h2&gt;
  
  
  local.settings.json: not config, environment variables
&lt;/h2&gt;

&lt;p&gt;A common mental model for &lt;code&gt;local.settings.json&lt;/code&gt; is that it works like &lt;code&gt;appsettings.json&lt;/code&gt;. It does not.&lt;/p&gt;

&lt;p&gt;Everything in the &lt;code&gt;Values&lt;/code&gt; section is read by Core Tools at startup and injected as process environment variables into both &lt;code&gt;func.exe&lt;/code&gt; and the worker process. That is the entire job of this file when running locally. In Azure, there is no &lt;code&gt;local.settings.json&lt;/code&gt; at all: the App Settings page in the portal (or the equivalent in Bicep/ARM/Terraform) sets those same environment variables directly on the hosted function app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File structure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"IsEncrypted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"FUNCTIONS_WORKER_RUNTIME"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotnet-isolated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UseDevelopmentStorage=true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"MyQueueConnection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UseDevelopmentStorage=true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"MyServiceBusConnection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;real-sb-connection-string&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"LocalHttpPort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7071&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CORS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CORSCredentials"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ConnectionStrings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AppDb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Server=localhost;Database=MyApp;Trusted_Connection=True;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two keys in &lt;code&gt;Values&lt;/code&gt; are required for every project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FUNCTIONS_WORKER_RUNTIME&lt;/code&gt;: must be &lt;code&gt;"dotnet-isolated"&lt;/code&gt; (not &lt;code&gt;"dotnet"&lt;/code&gt;, which is the in-process value; using it causes confusing startup failures)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AzureWebJobsStorage&lt;/code&gt;: required for every trigger type except HTTP. The host uses this storage account for timer leases, key management, and Durable Functions. Set to &lt;code&gt;"UseDevelopmentStorage=true"&lt;/code&gt; when Azurite is running.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;ConnectionStrings&lt;/code&gt; section gotcha&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ConnectionStrings&lt;/code&gt; is for Entity Framework and similar frameworks that call &lt;code&gt;IConfiguration.GetConnectionString("Name")&lt;/code&gt;. It is not for Functions trigger and binding configuration. If you put your Service Bus or Queue connection string here, the binding's &lt;code&gt;Connection&lt;/code&gt; property won't find it, and the error message won't point you at the right place. All binding connection strings must go in &lt;code&gt;Values&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the file is gitignored&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even if your local values only point at Azurite today, a real connection string will appear in this file at some point, whether you add it or a teammate does. Once committed, it lives in git history. The &lt;code&gt;.gitignore&lt;/code&gt; and &lt;code&gt;.funcignore&lt;/code&gt; that &lt;code&gt;func init&lt;/code&gt; generates both exclude &lt;code&gt;local.settings.json&lt;/code&gt; from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sharing the structure with the team&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The standard approach: commit a &lt;code&gt;local.settings.json.example&lt;/code&gt; file with the same structure but placeholder values. New team members copy it to &lt;code&gt;local.settings.json&lt;/code&gt; and fill in the real values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"IsEncrypted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"FUNCTIONS_WORKER_RUNTIME"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotnet-isolated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AzureWebJobsStorage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UseDevelopmentStorage=true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ServiceBusConnection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-service-bus-connection-string&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Document in the README which values work with Azurite and which require a real Azure service. Your teammates will thank you the first time they clone the repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading values in code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Direct access anywhere in your code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MyKey"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recommended approach: inject &lt;code&gt;IConfiguration&lt;/code&gt; and read by key name. Since the values are environment variables, the key name is the exact string in &lt;code&gt;Values&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderProcessor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IConfiguration&lt;/span&gt; &lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IConfiguration&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;connStr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"MyQueueConnection"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Part 6 covers structuring values for testability with &lt;code&gt;IOptions&amp;lt;T&amp;gt;&lt;/code&gt;, including the double-underscore separator needed for nested configuration in environment variables.&lt;/p&gt;




&lt;h2&gt;
  
  
  Azurite: your local storage account
&lt;/h2&gt;

&lt;p&gt;Azurite emulates the three Azure Storage services locally: Blob, Queue, and Table. Azure Functions uses these services internally, which is why Azurite has to be running even if your function code does not touch storage directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the host needs storage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Functions host uses Azure Storage for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timer trigger lease management&lt;/strong&gt;: prevents duplicate executions when multiple instances run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Function key storage&lt;/strong&gt;: host keys and function keys are persisted in blob storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durable Functions task hub&lt;/strong&gt;: state and message coordination&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event Hubs checkpoints&lt;/strong&gt;: tracks which events have been processed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;AzureWebJobsStorage&lt;/code&gt; in &lt;code&gt;local.settings.json&lt;/code&gt; is the connection string for this internal storage use. Only pure HTTP-only projects can skip it. Everything else needs Azurite running first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which service maps to which trigger&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Azurite service&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Used by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Blob service&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td&gt;Blob trigger, Blob input/output bindings, host key storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue service&lt;/td&gt;
&lt;td&gt;10001&lt;/td&gt;
&lt;td&gt;Queue trigger, Queue output binding, Durable Functions activity queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table service&lt;/td&gt;
&lt;td&gt;10002&lt;/td&gt;
&lt;td&gt;Table input/output bindings, Durable Functions instance state&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Azurite Table Storage is still in preview as of February 2026. It works for most local development scenarios but is not production-equivalent.&lt;/p&gt;

&lt;p&gt;You can start individual services if you want to be selective: &lt;code&gt;azurite-blob&lt;/code&gt;, &lt;code&gt;azurite-queue&lt;/code&gt;, or &lt;code&gt;azurite-table&lt;/code&gt; as separate commands. From the VS Code Command Palette: &lt;strong&gt;Azurite: Start Blob Service&lt;/strong&gt;, &lt;strong&gt;Azurite: Start Queue Service&lt;/strong&gt;, etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connection strings&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The shorthand that works when Azurite runs on the default ports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;UseDevelopmentStorage&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full explicit form, needed when Azurite is in Docker or using non-default ports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DefaultEndpointsProtocol&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;devstoreaccount1&lt;/code&gt; account name and the key above are the public well-known Azurite credentials, the same for every developer. They carry no security value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common gotchas&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Port conflicts&lt;/strong&gt;: if ports 10000, 10001, or 10002 are already in use, Azurite fails silently or with a cryptic error. Check with &lt;code&gt;lsof -i :10000&lt;/code&gt; (macOS/Linux) or &lt;code&gt;netstat -ano | findstr :10000&lt;/code&gt; (Windows). Run with &lt;code&gt;--blobPort 20000 --queuePort 20001 --tablePort 20002&lt;/code&gt; to use alternate ports, and update your connection strings accordingly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data location&lt;/strong&gt;: running &lt;code&gt;azurite&lt;/code&gt; with no &lt;code&gt;--location&lt;/code&gt; flag writes data files to the current working directory. The VS Code extension stores data in the workspace folder. Run &lt;strong&gt;Azurite: Clean&lt;/strong&gt; from the Command Palette to wipe all data and start fresh, which is useful when you want to test a clean startup sequence.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Docker networking&lt;/strong&gt;: &lt;code&gt;UseDevelopmentStorage=true&lt;/code&gt; resolves to &lt;code&gt;127.0.0.1&lt;/code&gt;, meaning the local machine. If your function host runs in a Docker container, &lt;code&gt;127.0.0.1&lt;/code&gt; points inside that container, not at Azurite running on your host. Use &lt;code&gt;host.docker.internal&lt;/code&gt; in the explicit connection string form instead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never publish &lt;code&gt;UseDevelopmentStorage=true&lt;/code&gt; to Azure&lt;/strong&gt;: the Functions host will fail to start. Azure App Settings needs a real storage account connection string, not the Azurite shorthand.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Debugging in VS Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Required extensions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two extensions are needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Azure Functions&lt;/strong&gt; (&lt;code&gt;ms-azuretools.vscode-azurefunctions&lt;/code&gt;): provides the &lt;code&gt;${command:azureFunctions.pickProcess}&lt;/code&gt; variable that makes F5 work, and the &lt;strong&gt;Execute Function Now...&lt;/strong&gt; command for triggering non-HTTP functions from the editor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C# Dev Kit&lt;/strong&gt; (&lt;code&gt;ms-dotnettools.csdevkit&lt;/code&gt;): the recommended C# extension for new setups. Installs the base C# extension (&lt;code&gt;ms-dotnettools.csharp&lt;/code&gt;) automatically, which provides the &lt;code&gt;coreclr&lt;/code&gt; debugger needed for attaching to the worker process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recommended additions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Azurite&lt;/strong&gt; (&lt;code&gt;Azurite.azurite&lt;/code&gt;): start and stop Azurite directly from the Command Palette without switching to a terminal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;REST Client&lt;/strong&gt; (&lt;code&gt;humao.rest-client&lt;/code&gt;): run &lt;code&gt;.http&lt;/code&gt; files against your functions from inside the editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The F5 workflow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you press F5 on a Functions project, the following sequence runs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;VS Code executes the &lt;code&gt;build (functions)&lt;/code&gt; task from &lt;code&gt;tasks.json&lt;/code&gt; (a &lt;code&gt;dotnet build&lt;/code&gt; with &lt;code&gt;--no-incremental&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The Azure Functions extension starts &lt;code&gt;func start&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;func.exe&lt;/code&gt; launches the worker process (&lt;code&gt;dotnet.exe&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The extension resolves the worker PID via &lt;code&gt;${command:azureFunctions.pickProcess}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;coreclr&lt;/code&gt; debugger attaches to the worker process&lt;/li&gt;
&lt;li&gt;Breakpoints activate&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9mmw9i7a0an1j7tfk2dm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9mmw9i7a0an1j7tfk2dm.png" alt="VS Code debug session paused at a breakpoint, with the func host running in the terminal and .NET worker threads visible in the Call Stack panel" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generated &lt;code&gt;launch.json&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;func init&lt;/code&gt; for a dotnet-isolated project generates this configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"configurations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Attach to .NET Functions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"coreclr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"attach"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"processId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${command:azureFunctions.pickProcess}"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;"request": "attach"&lt;/code&gt; is not a mistake. The worker process is started by &lt;code&gt;func.exe&lt;/code&gt;, not by VS Code, so VS Code cannot launch it. It can only attach to it after the fact. The &lt;code&gt;${command:azureFunctions.pickProcess}&lt;/code&gt; variable is what prompts the process picker if the extension cannot auto-detect the right PID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;tasks.json&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The extension generates five tasks. The critical one is the &lt;code&gt;func: host start&lt;/code&gt; task at the bottom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tasks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"clean (functions)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotnet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"clean"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/MyProject.csproj"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"/property:GenerateFullPaths=true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"/consoleloggerparameters:NoSummary"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"process"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"problemMatcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$msCompile"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"build (functions)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotnet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/MyProject.csproj"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"/property:GenerateFullPaths=true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"/consoleloggerparameters:NoSummary"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"process"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"clean (functions)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"isDefault"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"problemMatcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$msCompile"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"func"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"func: host start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"host start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"cwd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/bin/Debug/net10.0"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"isBackground"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"build (functions)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"problemMatcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$func-dotnet-isolated-watch"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;func&lt;/code&gt; task type points &lt;code&gt;cwd&lt;/code&gt; at the compiled output directory because the isolated worker is an executable that runs from there. The &lt;code&gt;problemMatcher&lt;/code&gt; is &lt;code&gt;$func-dotnet-isolated-watch&lt;/code&gt; (not &lt;code&gt;$func-dotnet-watch&lt;/code&gt;, which is for in-process). The &lt;code&gt;dependsOn&lt;/code&gt; chain means F5 triggers a build before starting the host.&lt;/p&gt;

&lt;p&gt;If you create a project via &lt;code&gt;func init&lt;/code&gt; on the CLI instead of through the VS Code extension, you may not get &lt;code&gt;.vscode&lt;/code&gt; files automatically. Run &lt;strong&gt;Azure Functions: Initialize Project for Use with VS Code&lt;/strong&gt; from the Command Palette to generate them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setting breakpoints and inspecting state&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click the gutter (left of line numbers) to set a breakpoint. When the function is triggered, execution pauses. From there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Variables panel&lt;/strong&gt;: all locals and parameters in scope&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hover over any identifier&lt;/strong&gt;: shows its current value inline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debug Console&lt;/strong&gt;: evaluates any C# expression against live state (LINQ works, method calls work)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call Stack panel&lt;/strong&gt;: the full call chain from the gRPC dispatcher into your function code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwug8gr0zqloj0qz8q6xd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwug8gr0zqloj0qz8q6xd.png" alt="VS Code Debug Console evaluating a C# expression against live function state" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing non-HTTP functions from VS Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Command Palette → &lt;strong&gt;Azure Functions: Execute Function Now...&lt;/strong&gt; → select &lt;strong&gt;Local project&lt;/strong&gt; → pick the function.&lt;/p&gt;

&lt;p&gt;This posts to the admin endpoint: &lt;code&gt;POST http://localhost:7071/admin/functions/{FunctionName}&lt;/code&gt; with &lt;code&gt;{"input": ""}&lt;/code&gt;. For queue triggers, set &lt;code&gt;input&lt;/code&gt; to the message body you want to inject.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common pitfall&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Opening a subfolder of your project instead of the folder containing &lt;code&gt;host.json&lt;/code&gt; causes F5 to fail without a clear error. The &lt;code&gt;.vscode&lt;/code&gt; folder is not found. Always open the project root.&lt;/p&gt;




&lt;h2&gt;
  
  
  Debugging in Visual Studio 2026
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Visual Studio 2026 (GA since November 2025) is required for .NET 10 development. Visual Studio 2022 cannot target &lt;code&gt;net10.0&lt;/code&gt;. Install Visual Studio 2026 from the standard Visual Studio installer.&lt;/p&gt;

&lt;p&gt;Install the &lt;strong&gt;Azure development&lt;/strong&gt; workload during setup or add it afterward via the Visual Studio Installer. This adds the Functions project templates and Core Tools integration. Update Core Tools separately when Visual Studio prompts, or through &lt;strong&gt;Tools | Options | Projects and Solutions | Azure Functions | Check for Updates&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The F5 workflow&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Visual Studio builds the project&lt;/li&gt;
&lt;li&gt;Visual Studio launches &lt;code&gt;func.exe&lt;/code&gt; (output visible in the terminal pane)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;func.exe&lt;/code&gt; starts the worker (&lt;code&gt;dotnet.exe&lt;/code&gt;) and reports the worker PID back over gRPC&lt;/li&gt;
&lt;li&gt;Visual Studio automatically attaches its debugger to the worker &lt;code&gt;dotnet.exe&lt;/code&gt; process&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No &lt;code&gt;launch.json&lt;/code&gt; is needed. Visual Studio handles the two-process attach automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Useful VS-specific features&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App Settings dialog&lt;/strong&gt;: right-click the project → &lt;strong&gt;Publish&lt;/strong&gt; → &lt;strong&gt;Hosting&lt;/strong&gt; → &lt;strong&gt;Manage Azure App Service settings&lt;/strong&gt;. It shows a side-by-side view of local &lt;code&gt;local.settings.json&lt;/code&gt; values vs. Azure App Settings, and lets you push individual values to Azure with one click, useful for keeping environments in sync without manually copying strings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote debugging&lt;/strong&gt;: attach to a deployed function app from Visual Studio. Requires Premium or App Service plan (not Consumption or Flex), Windows OS, and a Debug build. Available for 48 hours before it auto-disables. Use &lt;strong&gt;Attach to Process&lt;/strong&gt;, set the connection type to &lt;strong&gt;Microsoft Azure App Services&lt;/strong&gt;, then select &lt;code&gt;dotnet.exe&lt;/code&gt; in the process list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Known issue&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In some Core Tools versions, starting with debugging can take 60 seconds to attach while starting without debugging is instant. Updating Core Tools via &lt;strong&gt;Tools | Options | Azure Functions&lt;/strong&gt; resolves this in most cases.&lt;/p&gt;




&lt;h2&gt;
  
  
  Debugging in Rider
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Plugin requirement&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Azure Toolkit for Rider&lt;/strong&gt; is not bundled with Rider and is not part of the JetBrains marketplace defaults. Install it from &lt;strong&gt;Settings | Plugins | Marketplace&lt;/strong&gt;, search for "Azure Toolkit for Rider". The current version is v4.6.5 (November 2025), a full rewrite of v3.x. Existing v3.x run configurations are not forward-compatible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The run configuration trap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you open a Functions project, Rider generates two run configurations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;.NET Launch Settings Profile&lt;/strong&gt;: based on &lt;code&gt;launchSettings.json&lt;/code&gt;. This does not work for Azure Functions. Ignore it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Function Host&lt;/strong&gt;: the correct one. Rider calls &lt;code&gt;func.exe&lt;/code&gt; with the right flags internally.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F416v2vosjtftj6mhbjrs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F416v2vosjtftj6mhbjrs.png" alt="Rider run configuration dialog showing Azure Function Host and .NET Launch Settings Profile entries" width="800" height="613"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you see &lt;strong&gt;"Broken configuration due to unavailable plugin"&lt;/strong&gt; after installing or updating to v4.0, delete the old configuration and create a new "Azure Function Host" configuration from scratch via &lt;strong&gt;Run | Edit Configurations | + | Azure Function Host&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debugging workflow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Select the &lt;strong&gt;Azure Function Host&lt;/strong&gt; configuration and press Shift+F9. Rider:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Builds the project&lt;/li&gt;
&lt;li&gt;Starts &lt;code&gt;func.exe&lt;/code&gt; with &lt;code&gt;--dotnet-isolated-debug&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Reads the worker PID from the host's stdout&lt;/li&gt;
&lt;li&gt;Attaches the Rider debugger to the worker process&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each &lt;code&gt;[Function]&lt;/code&gt; method gets gutter icons: the bug icon starts that function in debug mode, and the play icon opens a &lt;code&gt;.http&lt;/code&gt; scratch file with an invocation request ready to send from Rider's built-in HTTP client.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmhwohlu7ta40u11vg7to.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmhwohlu7ta40u11vg7to.png" alt="Rider gutter icons on a function method — bug icon for debug, play icon for HTTP scratch file" width="509" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known bugs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Azure Functions host did not return isolated worker process id"&lt;/strong&gt;: Rider reads the PID from the host log output. If the logging level in &lt;code&gt;host.json&lt;/code&gt; is set above &lt;code&gt;Information&lt;/code&gt;, the PID line is suppressed and Rider cannot detect the process. Fix: set &lt;code&gt;"logging": { "logLevel": { "default": "Information" } }&lt;/code&gt; in &lt;code&gt;host.json&lt;/code&gt;. JetBrains has marked this as &lt;code&gt;wontfix&lt;/code&gt; upstream: the PID must come from the host log line.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Slow or failed debugger attach&lt;/strong&gt;: start without debugging via the Run button, then attach manually through &lt;strong&gt;Run | Attach to Process&lt;/strong&gt; and select the worker &lt;code&gt;dotnet&lt;/code&gt; process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Port configuration&lt;/strong&gt;: the Azure Function Host run configuration does not read the port from &lt;code&gt;launchSettings.json&lt;/code&gt;. Set the port directly in &lt;strong&gt;Run | Edit Configurations | Function host arguments&lt;/strong&gt;. The default 7071 is fine for most setups.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Hot reload and dotnet watch
&lt;/h2&gt;

&lt;p&gt;Hot reload support for Azure Functions isolated worker depends on which tool starts the session:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Start method&lt;/th&gt;
&lt;th&gt;What happens on save&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Visual Studio F5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;True hot reload (Edit and Continue). Most method body changes apply without restart.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;dotnet watch&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full process restart. All in-memory state is lost.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;func start&lt;/code&gt;&lt;/strong&gt; (and Rider's Azure Function Host config)&lt;/td&gt;
&lt;td&gt;No change detection. Stop and restart manually.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VS Code F5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full process restart when you stop and re-run.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Visual Studio hot reload&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Changes that apply without a restart:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code changes inside existing method bodies&lt;/li&gt;
&lt;li&gt;Adding new methods, properties, or fields to existing types&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Changes that require a full restart (rude edits):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding a new trigger function (new &lt;code&gt;[Function]&lt;/code&gt; attribute)&lt;/li&gt;
&lt;li&gt;Changing binding attributes (&lt;code&gt;[QueueTrigger]&lt;/code&gt;, &lt;code&gt;[BlobTrigger]&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Changing &lt;code&gt;Program.cs&lt;/code&gt; startup code&lt;/li&gt;
&lt;li&gt;Changes to &lt;code&gt;host.json&lt;/code&gt; or &lt;code&gt;local.settings.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Adding new types or changing method signatures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The binding attribute restriction is the most relevant one for daily Functions development: any time you add a new trigger or modify a binding, you need a full restart.&lt;/p&gt;

&lt;p&gt;One gotcha: mixed-mode debugging (the "Enable native code debugging" checkbox in project properties) breaks hot reload. If hot reload stopped working after enabling that option, that's why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;dotnet watch&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;Microsoft.Azure.Functions.Worker.Sdk&lt;/code&gt; 2.0.0+, &lt;code&gt;dotnet run&lt;/code&gt; is supported when Core Tools is installed. &lt;code&gt;dotnet watch&lt;/code&gt; wraps &lt;code&gt;dotnet run&lt;/code&gt; and restarts the process on file changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet watch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a full restart, not hot reload. Useful if you want automatic restarts without manually stopping and restarting &lt;code&gt;func start&lt;/code&gt;, but in-memory state does not survive between restarts. On Windows, requires the MSI or winget Core Tools installation (not npm).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;--dotnet-isolated-debug&lt;/code&gt; flag&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;func start &lt;span class="nt"&gt;--dotnet-isolated-debug&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This flag starts the worker process, pauses it immediately, and prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Azure Functions .NET Worker (PID: 28664) initialized in debug mode. Waiting for debugger to attach...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The process stays paused until you manually attach a debugger. Use it when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need to debug &lt;code&gt;Program.cs&lt;/code&gt; startup code before any function gets called (the worker pauses before startup completes, so you can attach and step through the entire initialization sequence)&lt;/li&gt;
&lt;li&gt;VS Code or Rider is not auto-attaching to the correct process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For typical VS Code or Rider development, you do not need this flag. The IDE extensions handle process detection automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing non-HTTP triggers locally
&lt;/h2&gt;

&lt;p&gt;Every locally running Functions host exposes an admin API. Any trigger type can be invoked through it, without waiting for the actual trigger to fire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The admin endpoint&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Timer trigger (input can be empty)&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:7071/admin/functions/CleanupFunction &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"input": ""}'&lt;/span&gt;

&lt;span class="c"&gt;# Queue trigger (input = the queue message body)&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:7071/admin/functions/OrderProcessor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"input": "{\"orderId\": \"ord-123\", \"amount\": 99.99}"}'&lt;/span&gt;

&lt;span class="c"&gt;# Blob trigger&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:7071/admin/functions/ImageProcessor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"input": "test-blob-content"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function name in the URL is the string in &lt;code&gt;[Function("...")]&lt;/code&gt;, not the C# method name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simulating real storage events&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The admin endpoint is fast for testing, but it bypasses the actual trigger mechanism. For testing the trigger polling behavior, use Azurite directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a message to an Azurite queue: the queue trigger picks it up on the next poll cycle (default: 2 seconds locally)&lt;/li&gt;
&lt;li&gt;Upload a blob to an Azurite container: the blob trigger fires&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Azure Storage Explorer&lt;/strong&gt; (the desktop app) and the &lt;strong&gt;Azure Storage&lt;/strong&gt; VS Code extension both work against Azurite. Point them at &lt;code&gt;http://127.0.0.1:10000&lt;/code&gt; and use the well-known &lt;code&gt;devstoreaccount1&lt;/code&gt; credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.http&lt;/code&gt; scratch files&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;requests.http&lt;/code&gt; in the project root for quick invocations during development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;### Trigger cleanup manually
POST http://localhost:7071/admin/functions/CleanupFunction
Content-Type: application/json

{"input": ""}

### Test HTTP endpoint
POST http://localhost:7071/api/orders
Content-Type: application/json

{
  "orderId": "ord-123",
  "customerId": "cust-456",
  "amount": 99.99
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;REST Client (VS Code), Rider's built-in HTTP client, and Visual Studio's native &lt;code&gt;.http&lt;/code&gt; editor all execute these files with a click.&lt;/p&gt;




&lt;h2&gt;
  
  
  Productivity tips
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Two log streams, two configs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the isolated worker model, logging is split across two configuration points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;host.json&lt;/code&gt;&lt;/strong&gt;: controls the Functions host logs (trigger polling, execution lifecycle, binding errors)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker-side&lt;/strong&gt; (&lt;code&gt;appsettings.json&lt;/code&gt; or &lt;code&gt;Program.cs&lt;/code&gt;): controls your &lt;code&gt;ILogger&amp;lt;T&amp;gt;&lt;/code&gt; calls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Changes to one do not affect the other. To see your &lt;code&gt;Information&lt;/code&gt;-level logs during local development without drowning in host noise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;host.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"logLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Host.Aggregator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Trace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Host.Results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;appsettings.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"MyFunctionApp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Debug"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Application Insights drops worker logs by default&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you add Application Insights to a local setup and your &lt;code&gt;ILogger&lt;/code&gt; calls stop appearing, this is why: Application Insights registers a default filter rule in the isolated worker model that drops &lt;code&gt;Information&lt;/code&gt; and &lt;code&gt;Debug&lt;/code&gt; logs. To remove it, update &lt;code&gt;Program.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker.Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Hosting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Logging&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddApplicationInsightsTelemetryWorkerService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureFunctionsApplicationInsights&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Application Insights registers a default filter rule that drops everything&lt;/span&gt;
&lt;span class="c1"&gt;// below Warning. Remove it to allow Information-level logs through.&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LoggerFilterOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;LoggerFilterRule&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;defaultRule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProviderName&lt;/span&gt;
        &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"Microsoft.Extensions.Logging.ApplicationInsights.ApplicationInsightsLoggerProvider"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultRule&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultRule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you prefer to avoid touching &lt;code&gt;Program.cs&lt;/code&gt;, you can configure Application Insights log levels through &lt;code&gt;appsettings.json&lt;/code&gt; instead. Add this file to your project root (set "Copy to Output Directory" to "Copy if newer"):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ApplicationInsights"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When using &lt;code&gt;FunctionsApplication.CreateBuilder&lt;/code&gt;, this file is loaded automatically and the filter removal code above is not needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;APPLICATIONINSIGHTS_CONNECTION_STRING&lt;/code&gt;, not the instrumentation key&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Microsoft ended instrumentation key ingestion support on March 31, 2025. For any project started after that date, use the connection string setting in &lt;code&gt;local.settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"APPLICATIONINSIGHTS_CONNECTION_STRING"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"InstrumentationKey=...;IngestionEndpoint=https://..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not set both. If both are present, newer SDKs ignore the key.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Override log levels without restarting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use double-underscore environment variable overrides in &lt;code&gt;local.settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"AzureFunctionsJobHost__logging__logLevel__default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Debug"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This overrides &lt;code&gt;host.json&lt;/code&gt; log levels without touching the file or restarting &lt;code&gt;func start&lt;/code&gt;. Useful for temporarily enabling verbose output to track down a specific issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;host.json&lt;/code&gt; queue settings for faster debugging&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"queues"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"batchSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"visibilityTimeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"00:00:05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"maxDequeueCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;batchSize: 1&lt;/code&gt;: processes one queue message at a time, making it much easier to follow execution in the logs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;visibilityTimeout: "00:00:05"&lt;/code&gt;: failed messages reappear in 5 seconds instead of the default 10 minutes, so you can iterate on error handling without waiting&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxDequeueCount: 3&lt;/code&gt;: messages move to the poison queue after 3 failures (default is 5)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Terminal aliases&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add to .zshrc / .bashrc&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;fstart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'func start'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;fstartd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'func start --dotnet-isolated-debug'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;fstartv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'func start --verbose'&lt;/span&gt;

&lt;span class="c"&gt;# Start Azurite in background, then start the function host&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;devstart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'azurite --silent --location /tmp/azurite &amp;amp; func start'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;You can now run any trigger type locally, attach a debugger to your actual function code, test non-HTTP functions without waiting for real messages, and reproduce storage-related behaviors against Azurite without touching a live Azure subscription.&lt;/p&gt;

&lt;p&gt;Part 5 goes deeper into the architecture you've been running locally: the isolated worker model. Why Microsoft created a separate worker process, what you gain over the old in-process model, and what the November 2026 end-of-support deadline means for existing projects.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Azure Functions for .NET Developers: Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/martin_oehlert/why-azure-functions-serverless-for-net-developers-707"&gt;Why Azure Functions? Serverless for .NET Developers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/martin_oehlert/your-first-azure-function-http-triggers-step-by-step-ib8"&gt;Your First Azure Function: HTTP Triggers Step-by-Step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: &lt;a href="https://dev.to/martin_oehlert/beyond-http-timer-queue-and-blob-triggers-5aj5"&gt;Beyond HTTP: Timer, Queue, and Blob Triggers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 4: Local Development Setup: Tools, Debugging, and Hot Reload (this article)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Part 5: &lt;a href="https://dev.to/martin_oehlert/understanding-the-isolated-worker-model-5gd4"&gt;Understanding the Isolated Worker Model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: &lt;a href="https://dev.to/martin_oehlert/configuration-done-right-settings-secrets-and-key-vault-3n7h"&gt;Configuration Done Right: Settings, Secrets, and Key Vault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: &lt;a href="https://dev.to/martin_oehlert/testing-azure-functions-unit-integration-and-local-1cml"&gt;Testing Azure Functions: Unit, Integration, and Local&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: &lt;a href="https://dev.to/martin_oehlert/deploying-to-azure-cicd-with-github-actions-141m"&gt;Deploying to Azure: CI/CD with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 9: &lt;a href="https://dev.to/martin_oehlert/azure-functions-observability-from-blind-spots-to-production-clarity-24j4"&gt;Azure Functions Observability: From Blind Spots to Production Clarity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: &lt;a href="https://dev.to/martin_oehlert/production-realities-when-azure-functions-stops-being-serverless-p2g"&gt;Production Realities: When Serverless Stops Being Serverless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>dotnet</category>
      <category>serverless</category>
    </item>
  </channel>
</rss>
