<?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>Azure Event Grid on Jeppe Spanggaard - Software Developer | .NET, Azure &amp; Microsoft 365</title><link>https://jeppe-spanggaard.dk/tags/event-grid/</link><description>Recent content in Azure Event Grid on Jeppe Spanggaard - Software Developer | .NET, Azure &amp; Microsoft 365</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Thu, 05 Nov 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jeppe-spanggaard.dk/tags/event-grid/index.xml" rel="self" type="application/rss+xml"/><item><title>What Is Azure Event Grid (and Why I Don't Poll)</title><link>https://jeppe-spanggaard.dk/blogs/what-is-azure-event-grid/</link><pubDate>Thu, 05 Nov 2026 00:00:00 +0000</pubDate><guid>https://jeppe-spanggaard.dk/blogs/what-is-azure-event-grid/</guid><description>Learn what Azure Event Grid is, how topics, subscriptions, and dead-lettering fit together, and when to pick it over a queue.</description><content:encoded><![CDATA[<p>My backend needed to know when new mail landed in a couple hundred monitored mailboxes. Inbox and sent items, so call it 400 folders. My first thought was delta requests: call Microsoft Graph&rsquo;s delta endpoint on a timer, get back only what changed since the last call, act on it. Polling, but with the diffing done server-side.</p>
<p>Do the math on that. 400 folders, polled every minute, is 576,000 Graph calls a day. Almost all of them return nothing new. And Graph doesn&rsquo;t hand out that kind of call budget for free - you&rsquo;ll be deep in 429 territory long before you get anywhere near &ldquo;every minute&rdquo;.</p>
<p>So I went push instead: Graph change notifications, delivered as HTTPS POSTs straight to an Azure Function - the setup from <a href="https://jeppe-spanggaard.dk/blogs/graph-mail-rich-notifications-filter-before-fetch/">my rich notifications post</a>. No polling, no wasted calls, mail arrives when mail arrives.</p>
<p>It worked, and it still had a smell. Every notification now targets my function directly, so every failure is mine to own: deploys, restarts, an hour of bad code, a burst the plan can&rsquo;t absorb. Graph retries a little, but the guarantees are thin, and at hundreds of thousands of deliveries a month even a small fail ratio is real mail quietly lost. I wanted delivery to be someone else&rsquo;s job, done smarter than I&rsquo;d do it on a deadline. That someone turned out to be Azure Event Grid, and this post is the mental model I wish I&rsquo;d had on day one. The concrete mail setup gets its own post later.</p>
<h2 id="push-dont-poll">Push, Don&rsquo;t Poll</h2>
<p>The polling model makes you responsible for the question: &ldquo;anything new?&rdquo; asked over and over, mostly answered with no. The push model flips it. The system where things happen publishes an event when they happen, and your code gets invoked with the event as input. Nothing runs when nothing happens.</p>
<p>That&rsquo;s the whole pitch, and it&rsquo;s an old idea - webhooks have done this for years. What Event Grid adds is the production plumbing you&rsquo;d otherwise write yourself: fan-out to multiple receivers, filtering, automatic retries with backoff, and a paper trail for the events that never made it.</p>
<h2 id="event-grid-is-a-router-not-a-queue">Event Grid Is a Router, Not a Queue</h2>
<p>This is the mental model that matters most, and the one I got wrong first.</p>
<p>Event Grid is a router. An event arrives at a topic, Event Grid looks at the subscriptions on that topic, pushes the event to every one that matches, and moves on. The event is not stored. There is no backlog to work through, no offset to rewind, no built-in replay. If you need to see an event again after it was delivered, that&rsquo;s on you.</p>
<p>A queue like Service Bus is the opposite: it holds messages until a consumer takes them, guarantees order if you ask for it, and lets a slow consumer catch up later. The usual shorthand is events versus messages. An event is a fact: &ldquo;a mail arrived&rdquo;, &ldquo;a blob was created&rdquo;. The publisher doesn&rsquo;t know or care who listens. A message is a command with an intended recipient: &ldquo;provision this site&rdquo;, &ldquo;send this invoice&rdquo;. My backend uses both, and they don&rsquo;t compete - Service Bus carries the commands, Event Grid carries the facts.</p>
<p>If you catch yourself wanting order, backlog, or replay, you wanted a queue. If you want &ldquo;poke my function when something happens, and don&rsquo;t make me poll&rdquo;, you want Event Grid.</p>
<h2 id="three-kinds-of-event-grid-topics">Three Kinds of Event Grid Topics</h2>
<p>A topic is the thing events are published to, and there are three flavors:</p>
<ul>
<li><strong>System topics</strong> belong to Azure resources. A storage account can raise <code>BlobCreated</code>, a resource group can raise deployment events. You don&rsquo;t create the publisher, you just subscribe.</li>
<li><strong>Custom topics</strong> are yours. Your own code POSTs events to an endpoint you own, and Event Grid routes them. Good for decoupling your own services.</li>
<li><strong>Partner topics</strong> are the interesting one for my scenario: an external service outside your subscription publishes into your Azure subscription. Microsoft Graph is such a partner, which is how mail notifications for 400 folders end up as events in my resource group without a single public webhook on my side.</li>
</ul>
<p>One detail that surprised me: partner topics route by name. All 400 of my Graph subscriptions point at one shared partner topic. You do not get - or want - a topic per source.</p>
<p><img src="https://jeppe-spanggaard.dk/images/what-is-azure-event-grid-1_hu_f198819c1b7ef874.webp" srcset="/images/what-is-azure-event-grid-1_hu_c8a25ee3f7e72c92.webp 480w, /images/what-is-azure-event-grid-1_hu_410914be78eb83ba.webp 720w, /images/what-is-azure-event-grid-1_hu_f198819c1b7ef874.webp 1200w" sizes="(max-width: 760px) 100vw, 720px"
    width="1200" height="223"
    alt="Partner topic metrics in the Azure portal showing published and delivered events over a week" style="background:url(data:image/webp;base64,UklGRkAAAABXRUJQVlA4IDQAAADwAgCdASoYAAQAP1mMuEupJKKYBACTFYT0gAAzI9HmQAD&#43;3kGzdxeGO1Qe7DiE51neKgAA) center/cover no-repeat" loading="lazy" decoding="async"></p>
<h2 id="event-subscriptions-do-the-filtering">Event Subscriptions Do the Filtering</h2>
<p>A topic without subscriptions is a mailbox nobody checks. An event subscription says &ldquo;from this topic, send events matching this filter to this handler&rdquo;. Handlers can be Azure Functions, webhooks, Service Bus queues, storage queues, and more.</p>
<p>Filtering earns its keep once one topic carries more than one event type. My partner topic carries both new-mail notifications and subscription lifecycle events, with one subscription per function. You can filter by event type on each subscription so a function only receives its kind - or do what I ended up doing: skip the portal filters, let both functions receive everything, and have each one check the event&rsquo;s <code>type</code> in code and drop what isn&rsquo;t for it. Same routing, but the decision lives in code you can read and debug.</p>
<p>Here&rsquo;s roughly what an event looks like on the wire. Partner topics use the CloudEvents schema, which is a small CNCF-standardized envelope:</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-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;specversion&#34;</span>: <span style="color:#e6db74">&#34;1.0&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#e6db74">&#34;8f0f0e58-2c5e-4c4a-9f5e-4a1b2c3d4e5f&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;/tenants/&lt;tenant-id&gt;/applications/&lt;app-id&gt;&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;Microsoft.Graph.MessageCreated&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;time&#34;</span>: <span style="color:#e6db74">&#34;2026-11-02T09:14:32Z&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;data&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;subscriptionId&#34;</span>: <span style="color:#e6db74">&#34;&lt;graph-subscription-id&gt;&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;changeType&#34;</span>: <span style="color:#e6db74">&#34;created&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;resource&#34;</span>: <span style="color:#e6db74">&#34;Users/&lt;user-id&gt;/Messages/&lt;message-id&gt;&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;clientState&#34;</span>: <span style="color:#e6db74">&#34;&lt;shared-secret&gt;&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>What&rsquo;s happening here?</strong></p>
<ol>
<li>The envelope (<code>id</code>, <code>source</code>, <code>type</code>, <code>time</code>) is standard CloudEvents. Every event, from any publisher, has these fields, which is what makes generic routing and filtering possible.</li>
<li><code>type</code> is what you route on, whether with a subscription filter or a check at the top of your handler. The mail types are documented in the <a href="https://learn.microsoft.com/en-us/azure/event-grid/outlook-events">Outlook events reference</a>.</li>
<li><code>data</code> is the publisher&rsquo;s payload and can be anything. Note what it is here: pointers, not the mail itself. Events are notifications, not cargo ships.</li>
<li><code>clientState</code> is a shared secret you set when subscribing, so your handler can check the event really originates from your subscription.</li>
</ol>
<p><img src="https://jeppe-spanggaard.dk/images/what-is-azure-event-grid-2_hu_b7718015536eca05.webp" srcset="/images/what-is-azure-event-grid-2_hu_c20a6b1d7f537ea7.webp 480w, /images/what-is-azure-event-grid-2_hu_50922d50b84dca17.webp 720w, /images/what-is-azure-event-grid-2_hu_b7718015536eca05.webp 1200w" sizes="(max-width: 760px) 100vw, 720px"
    width="1200" height="139"
    alt="Event subscriptions list on a topic" style="background:url(data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAABQAwCdASoYAAMAP1mMtkspJKKYBACTFYT0gAAzI9HjGLjFAAD&#43;3kGqHe5B&#43;gAA) center/cover no-repeat" loading="lazy" decoding="async"></p>
<h2 id="retries-and-dead-letters-when-delivery-fails">Retries and Dead-Letters: When Delivery Fails</h2>
<p>This is where Event Grid stops being &ldquo;just webhooks with extra steps&rdquo;.</p>
<p>If a handler fails - throws, times out, returns a 5xx - Event Grid retries with exponential backoff for up to 24 hours. Your function can be down for a deploy, or an hour, or an evening, and the events still arrive when it comes back. No code on your side, it&rsquo;s just what delivery means here.</p>
<p>When 24 hours run out, or the retry count is exhausted, the event is dead-lettered: written as a blob to a storage container you configure, together with the reason delivery failed. That container is your safety net and your crime scene evidence in one. Since Event Grid doesn&rsquo;t store or replay events, the dead-letter container is also the only place a lost event can be recovered from, which makes it worth monitoring - that&rsquo;s a whole post of its own later in this series.</p>
<p>One catch to design for: delivery is at-least-once. Retries mean your handler will occasionally see the same event twice, so make handling idempotent - dedupe on the event or message id rather than assuming exactly-once.</p>
<h2 id="what-event-grid-costs">What Event Grid Costs</h2>
<p>Event Grid&rsquo;s basic tier charges $0.60 per million operations, and the first 100,000 operations each month are free. My mail scenario pushes roughly 300K events a month, which lands somewhere around twenty cents. The pipeline is a rounding error.</p>
<p>The real costs show up downstream: Application Insights ingestion if you log a line per event, and whatever your handler talks to. In my case the constraint was never Event Grid, it was the Graph throttling budget and the write throughput of the archive behind it. Event Grid just makes sure the events get to the code; what the code does next is still your problem.</p>
<h2 id="gotchas">Gotchas</h2>
<ul>
<li><strong>No replay means no replay.</strong> If your handler had a bug and quietly swallowed events for a week, those events are gone unless they were dead-lettered. Design your handler to fail loudly on real errors so retries and dead-lettering can do their job.</li>
<li><strong>Delivery is at-least-once.</strong> The same event can arrive twice. Idempotent handlers aren&rsquo;t optional.</li>
<li><strong>Events are pointers, not payloads.</strong> The event tells you something happened, and mostly not everything about it. Plan for the follow-up read, or use a publisher feature that includes the data (Graph can, with encryption attached).</li>
<li><strong>Don&rsquo;t reach for Event Grid when you want a queue.</strong> Order, backlog, and consumer-paced processing are queue features. I&rsquo;ve seen the mismatch from both sides, and the workarounds are uglier than just using Service Bus.</li>
</ul>
<h2 id="wrapping-up">Wrapping Up</h2>
<p>Event Grid is push-based routing of facts: publishers raise events at topics, subscriptions filter and deliver them to handlers, retries cover about 24 hours, and dead-letters catch the rest. It saved me from 576,000 daily &ldquo;anything new?&rdquo; calls, and then took the delivery failures off my plate too. The one-line rule of thumb: events for facts, queues for commands, and never poll for what Azure will push to you.</p>
<p>Next up in this series: actually wiring Microsoft Graph mail notifications to an Azure Function through a partner topic.</p>
]]></content:encoded></item></channel></rss>