<?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: Marvin Drude</title>
    <description>The latest articles on DEV Community by Marvin Drude (@marvin_drude_d778a97ea3cf).</description>
    <link>https://dev.to/marvin_drude_d778a97ea3cf</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1530178%2F578be2e1-b366-4a7e-8a2a-73eb149afa1e.jpg</url>
      <title>DEV Community: Marvin Drude</title>
      <link>https://dev.to/marvin_drude_d778a97ea3cf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marvin_drude_d778a97ea3cf"/>
    <language>en</language>
    <item>
      <title>Allocations on the Wire: Building a Low-Allocation MQTT Broker with Trie Routing and .NET 10</title>
      <dc:creator>Marvin Drude</dc:creator>
      <pubDate>Wed, 05 Aug 2026 20:29:50 +0000</pubDate>
      <link>https://dev.to/marvin_drude_d778a97ea3cf/allocations-on-the-wire-building-a-low-allocation-mqtt-broker-with-trie-routing-and-net-10-5174</link>
      <guid>https://dev.to/marvin_drude_d778a97ea3cf/allocations-on-the-wire-building-a-low-allocation-mqtt-broker-with-trie-routing-and-net-10-5174</guid>
      <description>&lt;p&gt;In the previous post, we laid down the foundations of &lt;a href="https://github.com/MarvinDrude/Beskar.Networking" rel="noopener noreferrer"&gt;Beskar.Networking&lt;/a&gt; — an ultra-fast transport layer utilizing &lt;code&gt;System.IO.Pipelines&lt;/code&gt; and &lt;code&gt;PinnedBlockMemoryPool&lt;/code&gt; to move raw bytes over sockets with zero GC pressure.&lt;/p&gt;

&lt;p&gt;But raw transport is only half the battle. If you want to build a fully fledged server framework, you need to handle application-layer protocols. For this project, that protocol is &lt;strong&gt;MQTT&lt;/strong&gt; (supporting both MQTT v3.1.1 and MQTT v5.0 specifications).&lt;/p&gt;

&lt;p&gt;Moving from raw streams to a stateful message broker introduces a mountain of high-allocation operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;MQTT packet parsing &amp;amp; encoding&lt;/strong&gt;: Serializing headers, user properties, payloads, and variable integers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscription matching (Trie)&lt;/strong&gt;: Figuring out which client sessions match a published topic filter (like &lt;code&gt;sensors/+/temperature&lt;/code&gt; or &lt;code&gt;factory/#&lt;/code&gt;) under high concurrency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session state tracking&lt;/strong&gt;: Managing Quality of Service (QoS) flow state, unacknowledged publishes, offline message queues, and session takeover rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep-alive monitoring&lt;/strong&gt;: Tracking heartbeats for thousands of connected clients.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you resolve these with naive heap-allocated lists, string splits, and timers, your garbage collector will spend more cleanup cycles than the network card does shifting packets.&lt;/p&gt;

&lt;p&gt;Let's dive into how we tamed the heap to build a zero-allocation MQTT broker.&lt;/p&gt;




&lt;h2&gt;
  
  
  Topic routing without string allocations
&lt;/h2&gt;

&lt;p&gt;The core job of an MQTT broker is routing messages. When a message is published to &lt;code&gt;factory/line1/sensor3/temperature&lt;/code&gt;, the broker must locate all matching subscriber filters (e.g. &lt;code&gt;factory/+/+/temperature&lt;/code&gt; or &lt;code&gt;factory/#&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;A naive approach splits the topic string by &lt;code&gt;/&lt;/code&gt;, walks the parts, and matches them using regular expressions. Under a load of 100,000 messages per second, this creates millions of short-lived &lt;code&gt;string&lt;/code&gt; and &lt;code&gt;string[]&lt;/code&gt; allocations.&lt;/p&gt;

&lt;p&gt;To eliminate this, &lt;code&gt;Beskar.Networking&lt;/code&gt; implements a highly optimized, concurrent, &lt;strong&gt;UTF-8 byte-based Trie router&lt;/strong&gt;: the &lt;code&gt;MqttTrieSubscriptionRouter&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Stack-only topic level enumeration
&lt;/h3&gt;

&lt;p&gt;Instead of splitting strings, we walk the raw UTF-8 bytes of the topic. We use a custom &lt;code&gt;TopicLevelEnumerator&lt;/code&gt; (a stack-only &lt;code&gt;ref struct&lt;/code&gt; enumerator) to slice the bytes at each &lt;code&gt;/&lt;/code&gt; separator without extracting substrings onto the heap.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Alternate Lookups for zero-allocation dictionary access
&lt;/h3&gt;

&lt;p&gt;Inside each node of our Trie (&lt;code&gt;MqttTrieNode&lt;/code&gt;), we store children in a dictionary:&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;internal&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MqttTrieNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&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="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;byte&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="k"&gt;get&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;level&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;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="n"&gt;MqttTrieNode&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Children&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;field&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;byte&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="n"&gt;MqttTrieNode&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ByteArrayEqualityComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normally, looking up a node in &lt;code&gt;Dictionary&amp;lt;byte[], MqttTrieNode&amp;gt;&lt;/code&gt; with a slice of the incoming topic (represented as a &lt;code&gt;ReadOnlySpan&amp;lt;byte&amp;gt;&lt;/code&gt;) would require converting the span to a &lt;code&gt;byte[]&lt;/code&gt; array, allocating memory.&lt;/p&gt;

&lt;p&gt;We bypass this entirely by using &lt;strong&gt;Alternate Lookups&lt;/strong&gt; — a powerful optimization feature in modern .NET. This allows us to query our &lt;code&gt;byte[]&lt;/code&gt; dictionary using a &lt;code&gt;ReadOnlySpan&amp;lt;byte&amp;gt;&lt;/code&gt; 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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Children&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;lookup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetAlternateLookup&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ReadOnlySpan&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;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;lookup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&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="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="c1"&gt;// Only allocate byte[] if we must add a new node&lt;/span&gt;
   &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;levelBytes&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="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
   &lt;span class="n"&gt;child&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;MqttTrieNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;levelBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;levelBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&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;
  
  
  3. The Visitor Pattern for matching
&lt;/h3&gt;

&lt;p&gt;When a message is published, we traverse the Trie to collect matches. If we had to allocate a new &lt;code&gt;List&amp;lt;MqttSubscription&amp;gt;&lt;/code&gt; for every routing check, it would flood the GC.&lt;/p&gt;

&lt;p&gt;Instead, we use a &lt;strong&gt;visitor pattern&lt;/strong&gt; (&lt;code&gt;ISubscriptionVisitor&lt;/code&gt;). Matching traverses the Trie and invokes the visitor's &lt;code&gt;Visit()&lt;/code&gt; method inline:&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;interface&lt;/span&gt; &lt;span class="nc"&gt;ISubscriptionVisitor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;MqttSubscription&lt;/span&gt; &lt;span class="n"&gt;subscription&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;By making the visitor a stack-allocated &lt;code&gt;struct&lt;/code&gt;, the matching process invokes zero allocations and executes inside a thread-safe read lock:&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;void&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TVisitor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ReadOnlySpan&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;TVisitor&lt;/span&gt; &lt;span class="n"&gt;visitor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
   &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TVisitor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ISubscriptionVisitor&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;disposer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnterReadLock&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;enumerator&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;TopicLevelEnumerator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="nf"&gt;MatchRecursive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_rootNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;enumerator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;visitor&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;void&lt;/span&gt; &lt;span class="n"&gt;MatchRecursive&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TVisitor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
   &lt;span class="n"&gt;MqttTrieNode&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;TopicLevelEnumerator&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;TVisitor&lt;/span&gt; &lt;span class="n"&gt;visitor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TVisitor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ISubscriptionVisitor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="c1"&gt;// Check multi-level wildcard (#)&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;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MultiLevelWildcardChild&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Subscriptions&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;hashSubs&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;for&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;i&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;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;hashSubs&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;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="n"&gt;visitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hashSubs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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;// Exact match or single-level (+) wildcard traversal...&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;levels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MoveNext&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;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subscriptions&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;exactSubs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;for&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;i&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;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;exactSubs&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;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="n"&gt;visitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exactSubs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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="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;currentLevel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="c1"&gt;// Query child node with zero allocations using alternate lookup&lt;/span&gt;
   &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;alternateLookup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetAlternateLookup&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ReadOnlySpan&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;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;alternateLookup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentLevel&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;var&lt;/span&gt; &lt;span class="n"&gt;exactChild&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;nextLevels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;MatchRecursive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exactChild&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;nextLevels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;visitor&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;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SingleLevelWildcardChild&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;nextLevels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;MatchRecursive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SingleLevelWildcardChild&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;nextLevels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;visitor&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;h2&gt;
  
  
  Session lifecycle, QoS, and offline queuing
&lt;/h2&gt;

&lt;p&gt;MQTT demands strict state handling for QoS 1 and 2 messages. If a client connects with a persistent session (&lt;code&gt;CleanSession = false&lt;/code&gt;), the broker must:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deduplicate incoming packets (QoS 2).&lt;/li&gt;
&lt;li&gt;Queue outgoing publishes if the client is currently offline.&lt;/li&gt;
&lt;li&gt;Deliver those queued messages once the client reconnects.&lt;/li&gt;
&lt;li&gt;Support &lt;strong&gt;Session Takeover&lt;/strong&gt; — safely disconnecting an old, stale connection when a new client connects with the same Client ID.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1. Packet deduplication and tracking
&lt;/h3&gt;

&lt;p&gt;To track unacknowledged publishes, each session maintains a registry of active packet IDs. If a packet is sent, we store its identifier and track its status (Published, Acknowledged, Received, Released). These lookups are mapped to pooled state objects, avoiding new object allocations during standard confirmation flows.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Session Takeover and event pipelines
&lt;/h3&gt;

&lt;p&gt;When a client reconnects, the broker must coordinate session takeover:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Locate the existing active session.&lt;/li&gt;
&lt;li&gt;Signal the old transport connection to gracefully close (awaiting the DISCONNECT packet or forcefully severing the socket).&lt;/li&gt;
&lt;li&gt;Transfer the queued offline messages to the new session.&lt;/li&gt;
&lt;li&gt;Clean up resources associated with the old connection without leaking rented buffers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the start of the cleanup routine hooked into our server lifecycle events:&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;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;HandleSessionTakeover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MqttSession&lt;/span&gt; &lt;span class="n"&gt;oldSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MqttSession&lt;/span&gt; &lt;span class="n"&gt;newSession&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="n"&gt;TraceLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogNeutralInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Session takeover initiated for Client ID: {0}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oldSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

   &lt;span class="c1"&gt;// Await current pending publish queues and copy them&lt;/span&gt;
   &lt;span class="n"&gt;newSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TransferOfflineQueueFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oldSession&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

   &lt;span class="c1"&gt;// Trigger disconnect of the old session&lt;/span&gt;
   &lt;span class="n"&gt;oldSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisconnectGracefully&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;h2&gt;
  
  
  Optimizing Keep-Alives without task bloat
&lt;/h2&gt;

&lt;p&gt;Each MQTT client negotiates a keep-alive timeout interval (e.g., 60 seconds). If the broker doesn't receive a packet within that window, it must disconnect the client.&lt;/p&gt;

&lt;p&gt;Creating a dedicated &lt;code&gt;System.Threading.Timer&lt;/code&gt; or spawning a long-running &lt;code&gt;Task.Delay&lt;/code&gt; loop for every connected client would consume huge amounts of memory and CPU cycles when scaling to tens of thousands of active connections.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;Beskar.Networking&lt;/code&gt;, we manage heartbeats with a centralized, single-loop &lt;strong&gt;KeepAliveService&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each connection session records a &lt;code&gt;LastPacketReceivedTime&lt;/code&gt; timestamp using a fast system clock.&lt;/li&gt;
&lt;li&gt;The centralized service runs on a single background timer, checking active sessions in batches.&lt;/li&gt;
&lt;li&gt;We minimize heap allocation inside this check loop by utilizing pre-allocated or rented arrays to track stale connection IDs that need to be dropped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combined with our use of &lt;code&gt;ValueTask&lt;/code&gt; for async execution paths, this allows the broker to keep track of connection heartbeats with virtually zero CPU overhead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Protocols like MQTT are historically demanding to implement efficiently because they are stateful, packet-driven, and highly dynamic.&lt;/p&gt;

&lt;p&gt;By layering &lt;code&gt;MqttTrieSubscriptionRouter&lt;/code&gt; on top of our pipeline-driven transport, we proved that you can write a complex server with clean, interface-driven abstractions without compromising on speed or memory usage.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://marvindrude.com/blogs/beskar-networking/low-allocation-mqtt-broker" rel="noopener noreferrer"&gt;marvindrude.com&lt;/a&gt;.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Star and explore the open-source repository on GitHub: &lt;a href="https://github.com/MarvinDrude/Beskar.Networking" rel="noopener noreferrer"&gt;github.com/MarvinDrude/Beskar.Networking&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>performance</category>
      <category>networking</category>
      <category>csharp</category>
    </item>
  </channel>
</rss>
