<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Flex-Consumption on ND Teknik</title><link>https://ndteknik.com/tags/flex-consumption/</link><description>Recent content in Flex-Consumption on ND Teknik</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>Nikolaos Delis</copyright><lastBuildDate>Wed, 16 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://ndteknik.com/tags/flex-consumption/index.xml" rel="self" type="application/rss+xml"/><item><title>Serverless was the slow part; moving two Function Apps off Flex Consumption</title><link>https://ndteknik.com/posts/flex-consumption-cold-starts-in-a-message-chain/</link><pubDate>Wed, 16 Sep 2026 00:00:00 +0000</pubDate><guid>https://ndteknik.com/posts/flex-consumption-cold-starts-in-a-message-chain/</guid><description>&lt;img src="https://ndteknik.com/posts/flex-consumption-cold-starts-in-a-message-chain/featured.png" alt="Featured image of post Serverless was the slow part; moving two Function Apps off Flex Consumption" /&gt;&lt;p&gt;&lt;strong&gt;An order took 70 to 110 seconds to travel from one tenant to another. Warm, the same trip takes about two seconds.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I have written a lot of kind words about serverless on this blog, and I stand by them. This is the post where I describe the workload serverless was wrong for, and what it cost me to find out.&lt;/p&gt;
&lt;h2 id="the-chain"&gt;The chain
&lt;/h2&gt;&lt;p&gt;The transport management system I work on lets one tenant order a leg and another tenant execute it. In between sits an integration hub, a separate product with its own Function App. A dispatch goes through roughly this.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TMS tenant A ──Service Bus──► Hub receive ──Service Bus──► Hub mapping ──Service Bus──► Hub delivery ──Service Bus──► TMS tenant B
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Every arrow is a Service Bus hop and every box is a function. Status reports from the driver app walk a similar chain in the other direction. In QA, both Function Apps ran on Flex Consumption. It was the modern choice, it was cheap, and it scaled to zero between messages. All three facts turned out to matter.&lt;/p&gt;
&lt;h2 id="where-the-time-went"&gt;Where the time went
&lt;/h2&gt;&lt;p&gt;Flex scales each app to zero when nothing runs. The next message pays a host cold start, and for a .NET isolated app with forty odd functions and a few hundred megabytes of dependencies that start was 10 to 30 seconds in my measurements. One cold start is tolerable. The chain above pays it at every hop, in two different apps, and the hops are serial. A dispatch measured 20 to 34 seconds. A full order with its status replies measured 70 to 110 seconds.&lt;/p&gt;
&lt;p&gt;Nobody noticed in the first weeks because the testers sent one message at a time and went for coffee. Real users send a dispatch and then watch the screen.&lt;/p&gt;
&lt;p&gt;The first fix was a half fix, and I want to be honest about it. The hub app also runs Durable Functions, and a timer polled an external system every few minutes. On Flex that timer alone caused 15 host restarts in two hours, with the starter function at 1.2 seconds per run instead of a few milliseconds. I pinned one always ready instance to the durable scaling group. Durable got warm. The Service Bus and timer functions kept scaling from zero, because always ready instances on Flex are assigned per function group, not per app. The chain stayed slow.&lt;/p&gt;
&lt;h2 id="the-arithmetic"&gt;The arithmetic
&lt;/h2&gt;&lt;p&gt;Flex lets you buy always ready instances per group. To keep the whole chain warm I counted eight groups across the two apps. At the QA prices of the time that was about 168 USD per environment per month, for QA and production each. The point of serverless was supposed to be paying for nothing while idle; paying to never be idle felt like the wrong tool defending itself.&lt;/p&gt;
&lt;p&gt;So I looked at what the apps actually used. Seven days of telemetry showed about two busy instances on average and a host peak of 1.8 GB. That is a small dedicated plan. A B2 for the TMS app and a B1 for the hub, both always on, came to 39 USD per environment. The B2 alone keeps all 42 TMS functions warm for 26 USD. No function pays a cold start outside a deploy or platform maintenance, and the scale up path is a SKU change on the plan, B3 and then Premium v3 for autoscale, none of which recreate the app.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bicep" data-lang="bicep"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;plan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;Microsoft.Web/serverfarms@2023-12-01&amp;#39;&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;planName&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;resourceGroup&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nv"&gt;location&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;linux&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;planSku&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// { name: &amp;#39;B2&amp;#39;, tier: &amp;#39;Basic&amp;#39; }&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;properties&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;reserved&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;zoneRedundant&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// in the site config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;siteConfig&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;alwaysOn&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;linuxFxVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;DOTNET-ISOLATED|10.0&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;appSettings&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;FUNCTIONS_EXTENSION_VERSION&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;~4&amp;#39;&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;FUNCTIONS_WORKER_RUNTIME&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;dotnet-isolated&amp;#39;&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;WEBSITE_RUN_FROM_PACKAGE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;1&amp;#39;&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Two settings differences bit me on the way. A dedicated plan takes the runtime from &lt;code&gt;FUNCTIONS_WORKER_RUNTIME&lt;/code&gt; and &lt;code&gt;linuxFxVersion&lt;/code&gt;; Flex took it from &lt;code&gt;functionAppConfig&lt;/code&gt; and rejected those settings. And a dedicated plan keeps the deployed package on its own disk, so &lt;code&gt;WEBSITE_RUN_FROM_PACKAGE&lt;/code&gt; with no content share is enough.&lt;/p&gt;
&lt;h2 id="arm-will-not-convert-the-plan-for-you"&gt;ARM will not convert the plan for you
&lt;/h2&gt;&lt;p&gt;This is the part that turned an afternoon into a week. A site cannot change plan kind in place. ARM refuses the &lt;code&gt;serverFarmId&lt;/code&gt; change, and it refuses to turn an FC1 plan into a Basic one. The only path is to delete the app and the plan and let the deployment recreate both.&lt;/p&gt;
&lt;p&gt;The app holds no state, so deletion sounds harmless. It is not, for three reasons.&lt;/p&gt;
&lt;p&gt;Deleting the app deletes its managed identity. The new app gets a new principal. My role assignments are named after the site, not the principal, so the tombstoned assignments of the dead identity collide with the new ones and the deployment fails with &lt;code&gt;RoleAssignmentUpdateNotPermitted&lt;/code&gt;. The deploy script now lists assignments by principal id and removes them before the template runs. Not by assignee; once the identity is gone from Entra, the CLI cannot resolve it and exits with an error. The Cosmos SQL role assignments and the Managed Redis access policy assignment bind the principal too, so they go as well.&lt;/p&gt;
&lt;p&gt;The SQL and PostgreSQL users were created &lt;code&gt;FROM EXTERNAL PROVIDER&lt;/code&gt;, which binds the object id. The template cannot fix those. The script prints the steps at the end and a human runs them.&lt;/p&gt;
&lt;p&gt;In production the app integrates into a subnet, and the delegation of that subnet changes from &lt;code&gt;Microsoft.App/environments&lt;/code&gt; to &lt;code&gt;Microsoft.Web/serverFarms&lt;/code&gt;. The subnet accepts the new delegation only once the service association link of the Flex app is gone, and deleting a site without disconnecting it first can leave that link orphaned. The script disconnects first, deletes, then polls the subnet for up to five minutes. If the link is still there it prints the purge call and stops.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;az rest --method POST &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --uri &lt;span class="s2"&gt;&amp;#34;/subscriptions/&lt;/span&gt;&lt;span class="nv"&gt;$SUB&lt;/span&gt;&lt;span class="s2"&gt;/providers/Microsoft.Web/locations/swedencentral/purgeUnusedVirtualNetworkIntegration?api-version=2024-04-01&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --body &lt;span class="s2"&gt;&amp;#34;{&amp;#39;subnetResourceId&amp;#39;: &amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;$OLD_FUNC_SUBNET&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The guard in the deploy script checks the live plan tier, runs once, and never fires again. What if mode warns that the real run would delete first, because the what if diff shows an in place update that ARM would refuse.&lt;/p&gt;
&lt;h2 id="was-flex-wrong-or-was-i"&gt;Was Flex wrong, or was I?
&lt;/h2&gt;&lt;p&gt;Both, a little. Flex Consumption is a good product. For bursty HTTP, for a queue worker that does one job, for anything where a cold start is paid once and then amortized over a burst, I would pick it again tomorrow. I still run other apps on it.&lt;/p&gt;
&lt;p&gt;A chain is different. Latency compounds along it, idle time between hops is the normal state, and the thing a user waits for is the sum of every cold start on the path. For that shape, two small always on boxes are faster and cheaper than the serverless plan, and they are boring to operate.&lt;/p&gt;
&lt;p&gt;What I will do differently next time is measure the chain as a whole, from the first click to the last status, before choosing the hosting. I measured each function on its own, every one of them looked fine, and the sum was unusable.&lt;/p&gt;</description></item></channel></rss>