<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Azure on ND Teknik</title><link>https://ndteknik.com/categories/azure/</link><description>Recent content in Azure on ND Teknik</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>Nikolaos Delis</copyright><lastBuildDate>Thu, 23 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://ndteknik.com/categories/azure/index.xml" rel="self" type="application/rss+xml"/><item><title>I Moved a Production Durable Function to the Durable Task Scheduler</title><link>https://ndteknik.com/posts/moving-production-durable-function-durable-task-scheduler/</link><pubDate>Thu, 23 Jul 2026 00:00:00 +0000</pubDate><guid>https://ndteknik.com/posts/moving-production-durable-function-durable-task-scheduler/</guid><description>&lt;img src="https://ndteknik.com/posts/moving-production-durable-function-durable-task-scheduler/featured.png" alt="Featured image of post I Moved a Production Durable Function to the Durable Task Scheduler" /&gt;&lt;p&gt;&lt;strong&gt;I have used Durable Functions for years. Until recently, I had never used the Durable Task Scheduler.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That distinction matters. Durable Functions are the programming model: orchestrators, activities, durable timers, replay and all the constraints that come with them. The Durable Task Scheduler (DTS) is the managed backend that persists their state and dispatches their work.&lt;/p&gt;
&lt;p&gt;Historically, our Durable Functions used the default Azure Storage provider. Queues moved the work, Tables held instance state and history, and Blobs helped coordinate workers. It worked. I had no dramatic outage forcing me to replace it.&lt;/p&gt;
&lt;p&gt;Then Microsoft made the DTS Consumption SKU generally available. The pitch was difficult to ignore: a purpose-built high-performance backend, no storage infrastructure to operate, identity-based security, an excellent dashboard out of the box, and pay-per-action pricing with no idle cost.&lt;/p&gt;
&lt;p&gt;The final push, though, was a conversation. During Microsoft Build in San Francisco I had a shift at the Cloud Apps booth, where &lt;a class="link" href="https://learn.microsoft.com/en-us/shows/azure-friday/azure-functions-durable-task-scheduler" target="_blank" rel="noopener"
 &gt;Nick Greenfield&lt;/a&gt;, a senior product manager on the Azure Functions team, walked me through the Durable Task Scheduler. In hindsight, he was too humble about it. Nothing in that conversation prepared me for how good the dashboard and the performance would turn out to be.&lt;/p&gt;
&lt;p&gt;So I moved a real two-phase production workflow to it.&lt;/p&gt;
&lt;h2 id="what-this-workflow-actually-does"&gt;What this workflow actually does
&lt;/h2&gt;&lt;p&gt;The workflow distributes documents and, when requested, sends them to an external bookkeeping system. A Service Bus message can ask for either phase or both:&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-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Service Bus message
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Starter function
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── validate the tenant and batch references
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── build a deterministic instance ID
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── reject duplicate starts
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Orchestrator
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── Phase 1: document distribution
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ ├── load lightweight batch metadata
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ ├── page items by serialized byte size
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ ├── fan out deliveries
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ └── use durable timers for email throttling
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── Phase 2: bookkeeping
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── load lightweight batch metadata
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── page document references
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── group work by provider/company
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── send entries
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── persist per-document results
&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;Bookkeeping only starts if distribution succeeds, or when no distribution phase was requested. SQL remains the business source of truth. DTS coordinates the process; it does not become a second business database.&lt;/p&gt;
&lt;p&gt;This is a good Durable Functions workload because it combines fan-out/fan-in, external systems, rate limits, retries, partial failures and live progress. It is also exactly the sort of workflow where orchestration infrastructure becomes visible when it is slow or awkward to operate.&lt;/p&gt;
&lt;h2 id="the-migration-itself-was-almost-boring"&gt;The migration itself was almost boring
&lt;/h2&gt;&lt;p&gt;For a .NET isolated Function App, the visible code change was small. I added the Azure-managed provider extension:&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-xml" data-lang="xml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;1.9.0&amp;#34;&lt;/span&gt; &lt;span class="nt"&gt;/&amp;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;Then I selected the provider in &lt;code&gt;host.json&lt;/code&gt;:&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;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;extensions&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;durableTask&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;hubName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;document-workflows&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;storageProvider&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;azureManaged&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;connectionStringName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;DURABLE_TASK_SCHEDULER_CONNECTION_STRING&amp;#34;&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&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;The connection uses identity-based authentication and stays outside source control. The task hub name above is deliberately generic; the real resource and environment identifiers are irrelevant to the example.&lt;/p&gt;
&lt;p&gt;That is the pleasant part of the migration: the orchestrator and activity programming model does not change. Microsoft describes DTS as a drop-in provider for existing Durable Functions applications, although instance history cannot be migrated between storage providers. That makes the deployment boundary important: finish any old work you need to retain before pointing the app at the new, empty task hub.&lt;/p&gt;
&lt;p&gt;The provider switch was easy. Making the workflow a good citizen of the new backend took more thought.&lt;/p&gt;
&lt;h2 id="the-1-mb-limit-changed-how-i-thought-about-batches"&gt;The 1 MB limit changed how I thought about batches
&lt;/h2&gt;&lt;p&gt;DTS limits orchestrator inputs and outputs, activity inputs and outputs, external events and custom status to 1 MB each. A fixed page size such as &amp;ldquo;take 500 documents&amp;rdquo; is unsafe: 500 tiny objects might fit while 50 large ones might not.&lt;/p&gt;
&lt;p&gt;We page by the actual UTF-8 JSON size instead:&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;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&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-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;MaxPayloadBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1_048_576&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;ItemBudgetBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;800_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&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="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TakeWithinBudget&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;maxBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ItemBudgetBytes&lt;/span&gt;&lt;span class="p"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;page&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;size&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="c1"&gt;// []&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;itemBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SerializeToUtf8Bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&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;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;itemBytes&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;maxBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;itemBytes&lt;/span&gt;&lt;span class="p"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&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&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 800 KB working budget leaves room for the result envelope, cursor metadata and JSON framing. The orchestrator receives a page, fans out the relevant activities, waits for the page, records coarse progress, then asks for the next one.&lt;/p&gt;
&lt;p&gt;This also keeps orchestration history smaller. Durable state is not a convenient place to carry entire entity graphs.&lt;/p&gt;
&lt;h2 id="durable-timers-make-throttling-honest"&gt;Durable timers make throttling honest
&lt;/h2&gt;&lt;p&gt;One delivery channel permits 750 emails per hour. The original temptation was to load everything, split it into chunks of 750, and put a delay between the chunks.&lt;/p&gt;
&lt;p&gt;Inside an orchestrator, the delay must be durable:&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;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;emailWaveSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;750&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;moreEmails&lt;/span&gt;&lt;span class="p"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SendNextByteBoundedWave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emailWaveSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;moreEmails&lt;/span&gt;&lt;span class="p"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetCustomStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Phase&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;Distribution&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Channel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;Email&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;WaitingForRateWindow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&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;CreateTimer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentUtcDateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHours&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&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;The Function App is not burning compute for an hour. The orchestration is checkpointed and wakes when the durable timer fires. More importantly, the dashboard says that it is deliberately waiting for a rate window instead of merely looking stuck.&lt;/p&gt;
&lt;h2 id="retry-only-after-making-the-activity-safe-to-retry"&gt;Retry only after making the activity safe to retry
&lt;/h2&gt;&lt;p&gt;Distribution calls are naturally narrow: one item goes to one delivery activity. Transient operations get bounded retries.&lt;/p&gt;
&lt;p&gt;Bookkeeping is more dangerous. An external system may accept an invoice immediately before the Function host dies. Replaying the activity can then submit the same invoice twice.&lt;/p&gt;
&lt;p&gt;We did not solve that with a larger retry number. We first made the activity resumable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Already resolved documents are skipped on the next attempt.&lt;/li&gt;
&lt;li&gt;Per-document state is persisted as work progresses.&lt;/li&gt;
&lt;li&gt;Unresolved documents are moved out of the temporary &amp;ldquo;in progress&amp;rdquo; state when an attempt fails.&lt;/li&gt;
&lt;li&gt;Batch counters are recomputed from persisted document statuses instead of trusted as an in-memory total.&lt;/li&gt;
&lt;li&gt;Retries are selective and bounded: three attempts, starting at 30 seconds with exponential backoff.&lt;/li&gt;
&lt;li&gt;The remaining crash window relies on idempotency at the receiving provider.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point is important. Durable Functions give you reliable orchestration, not magical exactly-once delivery to an external system. If an API changes state and does not accept an idempotency key, there is always an uncomfortable edge between &amp;ldquo;the remote call succeeded&amp;rdquo; and &amp;ldquo;we persisted that it succeeded.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="another-small-migration-lesson-exceptions-are-data-now"&gt;Another small migration lesson: exceptions are data now
&lt;/h2&gt;&lt;p&gt;One of the first DTS failures was not in the business logic at all. An activity result tried to carry a live &lt;code&gt;Exception&lt;/code&gt; object back to the orchestrator.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;System.Exception&lt;/code&gt; is not a useful cross-process contract and does not round-trip through the isolated worker&amp;rsquo;s JSON serializer. The result now carries an error string instead:&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;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ActivityResult&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;Success&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;object?&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;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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string?&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;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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&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;If the orchestrator needs a failure, it creates a new exception from that serialized message. This makes the boundary explicit and avoids pretending an in-memory object can survive a durable hop.&lt;/p&gt;
&lt;h2 id="the-dashboard-is-not-a-side-feature"&gt;The dashboard is not a side feature
&lt;/h2&gt;&lt;p&gt;This may be my favourite part of DTS.&lt;/p&gt;
&lt;p&gt;The scheduler includes a dashboard where I can filter orchestrations, inspect their status and duration, see payloads and custom status, open their history, and view a timeline or flow diagram. I can also pause, resume, terminate, restart or raise an event when the orchestration supports it.&lt;/p&gt;
&lt;!-- Publication note: insert the redacted orchestration overview screenshot here. --&gt;
&lt;p&gt;Our custom status is intentionally small and coarse: phase, channel, total items, dispatched items, whether the workflow is waiting for the next email window, and a capped error message. We update it once per page, not once per document.&lt;/p&gt;
&lt;p&gt;That gives the dashboard useful business context without turning custom status into another event stream.&lt;/p&gt;
&lt;!-- Publication note: insert the redacted orchestration timeline screenshot here. --&gt;
&lt;p&gt;The timeline is particularly useful. In the captured example, the entire orchestration took just over a minute and the chart made it immediately obvious that almost all of that time belonged to the external bookkeeping call, not the scheduler.&lt;/p&gt;
&lt;p&gt;That is much more useful than staring at a pile of storage queues and wondering whether the workflow is progressing.&lt;/p&gt;
&lt;h2 id="what-production-looks-like-so-far"&gt;What production looks like so far
&lt;/h2&gt;&lt;p&gt;From the DTS cutover through 19 July, this task hub contained only this workflow:&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Measurement&lt;/th&gt;
					&lt;th style="text-align: right"&gt;Result&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Orchestration instances&lt;/td&gt;
					&lt;td style="text-align: right"&gt;596&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Completed&lt;/td&gt;
					&lt;td style="text-align: right"&gt;576&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Failed&lt;/td&gt;
					&lt;td style="text-align: right"&gt;20&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Completion rate&lt;/td&gt;
					&lt;td style="text-align: right"&gt;96.6%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Billable scheduler actions&lt;/td&gt;
					&lt;td style="text-align: right"&gt;9,339&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Orchestrator actions&lt;/td&gt;
					&lt;td style="text-align: right"&gt;4,968&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Activity actions&lt;/td&gt;
					&lt;td style="text-align: right"&gt;4,365&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Timer actions&lt;/td&gt;
					&lt;td style="text-align: right"&gt;6&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Peak actions in one day&lt;/td&gt;
					&lt;td style="text-align: right"&gt;7,478&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Peak pending activities&lt;/td&gt;
					&lt;td style="text-align: right"&gt;60&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Peak pending orchestrators&lt;/td&gt;
					&lt;td style="text-align: right"&gt;1&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Connected workers&lt;/td&gt;
					&lt;td style="text-align: right"&gt;1&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The 20 failures are workflow outcomes, not 20 scheduler outages. They include failures surfaced by our own activities and external integrations, which is exactly what I want the dashboard to make visible.&lt;/p&gt;
&lt;p&gt;The usage pattern also explains why Consumption fits. Most days are quiet, then a batch arrives and the workload spikes. One worker handled the measured load. The activity queue briefly reached 60 pending items, while the orchestrator queue barely formed at all and drained again.&lt;/p&gt;
&lt;h2 id="what-did-it-cost"&gt;What did it cost?
&lt;/h2&gt;&lt;p&gt;The Consumption SKU charges for dispatched actions. &lt;code&gt;TotalActions&lt;/code&gt; is the billable metric and is the sum of orchestrator, activity, timer and entity actions.&lt;/p&gt;
&lt;p&gt;At the Microsoft retail price for our Azure region on 20 July 2026, &lt;strong&gt;$0.0039 per action&lt;/strong&gt;, the scheduler calculation is simple:&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-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;9,339 actions × $0.0039 = $36.42
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$36.42 ÷ 596 orchestrations ≈ $0.061 per orchestration
&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;So the managed scheduler, persisted orchestration state and built-in dashboard cost roughly &lt;strong&gt;six cents per orchestration&lt;/strong&gt; during this measured period.&lt;/p&gt;
&lt;p&gt;That is an estimate based on Microsoft retail pricing, not an invoice. It excludes Azure Functions compute, taxes, currency conversion and any agreement-specific discounts. The Function App is billed separately, and Durable orchestrator replays can also count as Function executions on the Consumption hosting plan.&lt;/p&gt;
&lt;p&gt;Still, for this workload, roughly $36 for the scheduler is an easy decision.&lt;/p&gt;
&lt;h2 id="is-it-actually-faster"&gt;Is it actually faster?
&lt;/h2&gt;&lt;p&gt;Microsoft&amp;rsquo;s own early benchmark found DTS roughly five times faster than the Azure Storage provider for a deliberately simple sequential workload. Microsoft also warns that the result was from an early preview configuration and should be treated as a rough comparison, not a universal promise.&lt;/p&gt;
&lt;p&gt;Our production system &lt;strong&gt;feels much faster&lt;/strong&gt; after the switch.&lt;/p&gt;
&lt;p&gt;That is the strongest claim I can defend.&lt;/p&gt;
&lt;p&gt;The batches before and after the migration contained wildly different numbers and kinds of documents. We also changed paging and retry behaviour around the same time, and our tracing configuration was not identical on both sides of the cutover. A chart comparing those request durations would look scientific while proving very little.&lt;/p&gt;
&lt;p&gt;So I am not publishing one.&lt;/p&gt;
&lt;p&gt;The honest version is: Microsoft has controlled benchmark evidence for higher backend throughput, and our subjective production experience agrees with its direction. We do not have an apples-to-apples benchmark of our own workflow.&lt;/p&gt;
&lt;h2 id="consumption-is-the-right-sku-for-now"&gt;Consumption is the right SKU, for now
&lt;/h2&gt;&lt;p&gt;The Consumption SKU currently supports up to 500 dispatched actions per second, retains orchestration data for up to 30 days, and has no base fee or idle charge. That matches our bursty workload well.&lt;/p&gt;
&lt;p&gt;Dedicated is a different trade-off. It provides reserved capacity, up to 2,000 actions per second per Capacity Unit, up to 90 days of retention, and high availability when configured with three Capacity Units. If this workflow becomes continuously busy, requires longer history, or needs the higher SLA, I will revisit the choice.&lt;/p&gt;
&lt;p&gt;Right now, buying dedicated capacity for a worker that spends most of its day idle would be solving the wrong problem.&lt;/p&gt;
&lt;h2 id="what-i-would-carry-into-the-next-migration"&gt;What I would carry into the next migration
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Keep durable payloads small from the start.&lt;/strong&gt; Page by serialized bytes, not an arbitrary item count.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make external effects idempotent before adding retries.&lt;/strong&gt; Retry policies cannot repair an unsafe activity contract.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use custom status as a dashboard summary.&lt;/strong&gt; A few meaningful checkpoints beat thousands of status writes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build deterministic instance IDs.&lt;/strong&gt; They make Service Bus redelivery and duplicate detection manageable, but remember the 100-character DTS limit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep tracing consistent across the cutover.&lt;/strong&gt; Otherwise your before-and-after telemetry will not be comparable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do not confuse orchestration failures with scheduler failures.&lt;/strong&gt; A managed backend should expose your application failures clearly, not make them disappear.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The Durable Task Scheduler did not change how I write orchestrators. It changed how little infrastructure I have to think about while they run.&lt;/p&gt;
&lt;p&gt;For a few lines of configuration, we got a purpose-built backend, identity-based connectivity, a useful operational dashboard and a Consumption model that matches our bursts. The code still has to respect determinism, payload limits, idempotency and the realities of unreliable external systems.&lt;/p&gt;
&lt;p&gt;That seems like the right division of responsibility.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Thanks to Nick Greenfield for the conversation at Build that finally got me to take the leap.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Featured image:&lt;/strong&gt; Durable Task Scheduler dashboard screenshot from Microsoft&amp;rsquo;s &lt;a class="link" href="https://techcommunity.microsoft.com/blog/appsonazureblog/announcing-the-public-preview-launch-of-azure-functions-durable-task-scheduler/4389670" target="_blank" rel="noopener"
 &gt;public preview announcement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://techcommunity.microsoft.com/blog/appsonazureblog/the-durable-task-scheduler-consumption-sku-is-now-generally-available/4506682" target="_blank" rel="noopener"
 &gt;The Durable Task Scheduler Consumption SKU is generally available&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/en-us/azure/durable-task/scheduler/durable-task-scheduler" target="_blank" rel="noopener"
 &gt;Durable Task Scheduler overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/en-us/azure/durable-task/scheduler/durable-task-scheduler-dashboard" target="_blank" rel="noopener"
 &gt;Durable Task Scheduler dashboard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/en-us/azure/durable-task/common/durable-task-metrics" target="_blank" rel="noopener"
 &gt;Action metrics and billing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/en-us/azure/durable-task/scheduler/durable-task-scheduler-billing" target="_blank" rel="noopener"
 &gt;Durable Task Scheduler billing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/en-us/azure/durable-task/scheduler/durable-task-scheduler-work-item-throughput" target="_blank" rel="noopener"
 &gt;DTS throughput benchmark&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/en-us/rest/api/cost-management/retail-prices/azure-retail-prices" target="_blank" rel="noopener"
 &gt;Microsoft Azure Retail Prices API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>