<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Graph Webhooks and Notifications on Jeppe Spanggaard - Software Developer | .NET, Azure &amp; Microsoft 365</title><link>https://jeppe-spanggaard.dk/tags/webhooks/</link><description>Recent content in Graph Webhooks and Notifications on Jeppe Spanggaard - Software Developer | .NET, Azure &amp; Microsoft 365</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Tue, 14 Oct 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://jeppe-spanggaard.dk/tags/webhooks/index.xml" rel="self" type="application/rss+xml"/><item><title>Optimize SharePoint Webhooks: Debouncing User Actions</title><link>https://jeppe-spanggaard.dk/blogs/optimize-sharepoint-webhooks-debouncing-user-actions/</link><pubDate>Tue, 14 Oct 2025 00:00:00 +0000</pubDate><guid>https://jeppe-spanggaard.dk/blogs/optimize-sharepoint-webhooks-debouncing-user-actions/</guid><description>Learn how to effectively debounce SharePoint webhooks to optimize performance and reduce unnecessary operations when users frequently click save.</description><content:encoded><![CDATA[<p>Have you ever watched a user frantically editing a SharePoint list item? Click save, make another tiny change, click save again, then realize they made a typo and&hellip; click save once more. Meanwhile, your perfectly crafted webhook is firing off like a machine gun, triggering expensive operations for every single keystroke (okay, not literally every keystroke—but you get the idea).</p>
<p>This is exactly the problem I ran into while building a system where SharePoint list changes needed to trigger updates to site titles. Users would edit customer information, and I wanted the customer&rsquo;s SharePoint site title to reflect those changes. But I definitely didn&rsquo;t want to rename a site five times in thirty seconds just because someone couldn&rsquo;t decide between &ldquo;Acme Corp&rdquo; and &ldquo;ACME Corporation.&rdquo;</p>
<p>The solution? Debouncing. Just like the <code>useDebounce</code> hook that React developers know and love, but for SharePoint webhooks.</p>
<h2 id="the-business-problem">The Business Problem</h2>
<p>Let me paint you the picture. We have a UI that updates SharePoint list items every time a user makes a change. When they modify a customer&rsquo;s name, we want to update the title of that customer&rsquo;s SharePoint site. Simple enough, right?</p>
<p>Wrong.</p>
<p>Users don&rsquo;t make one clean edit and walk away. They edit the name, realize they want to change the format, edit it again, spot a typo, fix that too, and maybe adjust the capitalization for good measure. Each of these changes triggers our SharePoint webhook, which in turn tries to update the site title.</p>
<p>Not only is this inefficient, but it&rsquo;s also potentially problematic. Site title updates aren&rsquo;t instant operations, and we definitely don&rsquo;t want them queuing up and executing out of order.</p>
<p>What we really want is to wait until the user is <em>done</em> making changes, then process all their edits as one logical operation.</p>
<h2 id="enter-the-debounce-pattern">Enter the Debounce Pattern</h2>
<p>The concept is borrowed straight from the front-end world. Instead of reacting to every single change immediately, we wait. If another change comes in within our debounce window, we cancel the previous operation and reset the timer.</p>
<p>Here&rsquo;s the entry point to my implementation:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">WebhookProcessor</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">async</span> Task HandleListChangeAsync(<span style="color:#66d9ef">string</span> itemId, <span style="color:#66d9ef">string</span> listId) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">using</span> (DebounceScheduler debouncer = <span style="color:#66d9ef">new</span> DebounceScheduler()) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> message = <span style="color:#66d9ef">new</span> ServiceBusMessage() {
</span></span><span style="display:flex;"><span>                ContentType = <span style="color:#e6db74">&#34;application/json&#34;</span>,
</span></span><span style="display:flex;"><span>            };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> debouncer.DebounceAsync(itemId, listId, message, TimeSpan.FromMinutes(<span style="color:#ae81ff">5</span>));
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Simple on the surface, but there&rsquo;s quite a bit happening under the hood.</p>
<h2 id="the-architecture-azure-service-bus--table-storage">The Architecture: Azure Service Bus + Table Storage</h2>
<p>I built this debounce system using two Azure services:</p>
<ol>
<li><strong>Azure Service Bus</strong> for scheduled message delivery</li>
<li><strong>Azure Table Storage</strong> for tracking sequence numbers</li>
</ol>
<p>Here&rsquo;s why this combination works so well:</p>
<h3 id="azure-service-bus-scheduled-messages">Azure Service Bus Scheduled Messages</h3>
<p>Service Bus has a fantastic feature called scheduled messages. You can schedule a message to be delivered at a specific time in the future, and critically, you can cancel that scheduled message if needed.</p>
<p>This is perfect for debouncing. When a webhook fires, I schedule a message to be processed in 5 minutes. If another webhook fires for the same item before those 5 minutes are up, I cancel the previously scheduled message and schedule a new one.</p>
<h3 id="table-storage-for-state-management">Table Storage for State Management</h3>
<p>The tricky part is remembering which messages you&rsquo;ve scheduled so you can cancel them later. That&rsquo;s where Table Storage comes in.</p>
<p>The core logic looks something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">internal</span> <span style="color:#66d9ef">async</span> Task DebounceAsync(<span style="color:#66d9ef">string</span> entityId, <span style="color:#66d9ef">string</span> listId, ServiceBusMessage message, TimeSpan delay) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Get any existing scheduled message for this entity</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> oldSeq = <span style="color:#66d9ef">await</span> store.GetSequenceNumberAsync(entityId);
</span></span><span style="display:flex;"><span>   
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (oldSeq.HasValue) {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Cancel the previous scheduled message</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">await</span> _sender.CancelScheduledMessageAsync(oldSeq.Value);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">await</span> store.DeleteSequenceNumberAsync(entityId);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Schedule a new message</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> scheduledTime = DateTimeOffset.UtcNow.Add(delay);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">long</span> newSeq = <span style="color:#66d9ef">await</span> _sender.ScheduleMessageAsync(message, scheduledTime);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Store the new sequence number for future cancellation</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> store.SetSequenceNumberAsync(entityId, newSeq, listId);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>For each item being debounced, I store:</p>
<ul>
<li>The sequence number of the scheduled Service Bus message</li>
<li>The list ID</li>
</ul>
<p>When a new change comes in, I can look up the previous sequence number, cancel that message, and schedule a new one.</p>
<h2 id="the-flow-in-action">The Flow in Action</h2>
<p>Let&rsquo;s walk through what happens when a user edits a customer:</p>
<ol>
<li>User changes customer name → SharePoint webhook fires</li>
<li>My webhook Azure Function receives the notification and queues it</li>
<li>The webhook queue processor calls <code>WebhookProcessor.HandleListChangeAsync()</code></li>
<li><code>DebounceScheduler</code> checks Table Storage for any existing scheduled message</li>
<li>If one exists, it gets cancelled via the Service Bus sequence number</li>
<li>A new message is scheduled for 5 minutes from now</li>
<li>The new sequence number is saved to Table Storage</li>
</ol>
<p>If the user makes another change within 5 minutes, steps 4-7 repeat, effectively resetting the countdown.</p>
<p>Once 5 minutes pass without any changes, the scheduled message is delivered to our debounce queue, and the actual processing begins.</p>
<h2 id="when-sharepoint-gets-slow-really-slow">When SharePoint Gets Slow (Really Slow)</h2>
<p>Here&rsquo;s where things got interesting. During testing, I discovered that SharePoint webhooks aren&rsquo;t always as snappy as you&rsquo;d expect.</p>
<p>One evening, I made a change to a list item and noticed that both my webhook and a Power Automate flow (each set up for the same list) didn&rsquo;t fire until 22 minutes later. The timing was unusual, especially since this happened right when the customer&rsquo;s backup solution started running.</p>
<p>I can&rsquo;t say for sure what caused the delay, and I don&rsquo;t want to speculate or draw any conclusions based on this single incident. However, it does highlight an important consideration: webhook delivery isn&rsquo;t guaranteed to be immediate.</p>
<h2 id="trade-offs-and-considerations">Trade-offs and Considerations</h2>
<p>Like any architectural decision, this approach comes with trade-offs:</p>
<p><strong>Pros:</strong></p>
<ul>
<li>Dramatically reduces unnecessary processing</li>
<li>Handles the &ldquo;fidgety user&rdquo; problem elegantly</li>
<li>Resilient to webhook delivery delays</li>
<li>Scales well (Service Bus handles the heavy lifting)</li>
<li>Works across multiple function instances</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>Adds latency (minimum 5-minute delay)</li>
<li>Requires additional Azure services (cost)</li>
<li>More complex than direct processing</li>
<li>Potential for message loss (though Service Bus is pretty reliable)</li>
</ul>
<p><strong>Alternative Approaches I Considered:</strong></p>
<ol>
<li><strong>In-memory debouncing</strong>: Would work for a single instance, but doesn&rsquo;t scale across multiple Azure Function instances</li>
<li><strong>Redis-based debouncing</strong>: Would work, but adds another dependency and requires more custom logic</li>
<li><strong>Database polling</strong>: Could work, but feels clunky and less efficient than Service Bus scheduled messages</li>
<li><strong>Simple delays</strong>: Just wait X seconds before processing, but this doesn&rsquo;t handle multiple rapid changes</li>
</ol>
<p>The Service Bus + Table Storage approach won because it&rsquo;s distributed by nature, leverages existing Azure services we were already using, and handles cancellation elegantly.</p>
<h2 id="wrapping-up">Wrapping Up</h2>
<p>Debouncing SharePoint webhooks with Azure Service Bus and Table Storage has made our operations more efficient and resilient to user behavior and platform quirks.</p>
]]></content:encoded></item></channel></rss>