<?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: Davi Orlandi</title>
    <description>The latest articles on DEV Community by Davi Orlandi (@dvorlandi).</description>
    <link>https://dev.to/dvorlandi</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%2F1730896%2Fa4867ff8-3636-4f5e-8580-4d0a3c651593.jpg</url>
      <title>DEV Community: Davi Orlandi</title>
      <link>https://dev.to/dvorlandi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dvorlandi"/>
    <language>en</language>
    <item>
      <title>Atlas Online Archive for Large Collections: Keep Hot Data Hot</title>
      <dc:creator>Davi Orlandi</dc:creator>
      <pubDate>Mon, 05 Oct 2026 14:37:56 +0000</pubDate>
      <link>https://dev.to/dvorlandi/atlas-online-archive-for-large-collections-keep-hot-data-hot-33fm</link>
      <guid>https://dev.to/dvorlandi/atlas-online-archive-for-large-collections-keep-hot-data-hot-33fm</guid>
      <description>&lt;p&gt;Look at almost any production cluster and you will find the same shape. A thin slice of recent data gets nearly all reads and writes. Everything older is rarely touched but still sits on premium storage, inflates indexes, and eventually forces a bigger tier. You pay hot prices to store silence.&lt;/p&gt;

&lt;p&gt;I have lived that bill. Compliance forbade TTL deletion. A handmade export pipeline meant owning a second query path forever. Atlas Online Archive became the managed middle ground: move eligible documents to MongoDB-managed object storage and keep them queryable through federation. The personal stake is not "cool feature." It is whether you can stop buying CPU and disks for data nobody opens.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works in practice
&lt;/h2&gt;

&lt;p&gt;You define an archive rule, usually a date age, optionally a custom filter. Atlas runs background jobs that copy matching documents to object storage using the partition fields you chose. After a confirmed copy, documents leave the cluster. Federated connection strings query archive-only or cluster-plus-archive under the same namespace.&lt;/p&gt;

&lt;p&gt;A few properties matter more than the marketing blurbs. Dedicated clusters (M10+) only. Archived data is read-oriented; do not treat it as your OLTP write plane. Jobs are asynchronous and consume cluster resources while they run. Your normal URI sees only the live cluster; historical access needs the federated endpoint. That last sentence is where teams get surprised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Good candidates versus poor ones
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Good candidates&lt;/th&gt;
&lt;th&gt;Poor candidates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Event logs and audit trails&lt;/td&gt;
&lt;td&gt;User profiles that keep changing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orders older than a stable hot window&lt;/td&gt;
&lt;td&gt;Tiny reference collections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IoT readings and metrics&lt;/td&gt;
&lt;td&gt;Data that must stay low-latency forever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Closed tickets past the edit window&lt;/td&gt;
&lt;td&gt;Documents refunded or edited indefinitely&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ask: once a document is old, is it effectively immutable? If not, push the archive age past the last realistic edit, or filter on a terminal status. Also ask whether anyone needs the data at all. If the answer is no, a TTL index is simpler than archiving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the rule without painting yourself into a corner
&lt;/h2&gt;

&lt;p&gt;Date-based rules are the common case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Archive documents where createdAt is older than 90 days
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Index the predicate fields so the job does not scan forever:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Custom filters help when coldness is not pure age (&lt;code&gt;status in terminal states&lt;/code&gt; and &lt;code&gt;closedAt&lt;/code&gt; old enough). Prefer moving windows over hard-coded calendar dates that rot. Start conservative (365 days) and ratchet hotter as confidence grows. Prefer an immutable date field. Archiving on &lt;code&gt;updatedAt&lt;/code&gt; surprises you when batch jobs touch old rows and suddenly nothing looks cold enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Partition fields decide archive query cost
&lt;/h2&gt;

&lt;p&gt;Partitions let federation skip irrelevant object-storage slices. The date field is typically one partition. Add a small number of extra fields ordered by how often historical queries filter on them (&lt;code&gt;customerId&lt;/code&gt;, &lt;code&gt;region&lt;/code&gt;). Moderate cardinality helps. Unique-per-document fields help little. Partitions are hard to change later. List the three real archive queries your team runs before you click save. That conversation has prevented more regret than any checklist I own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Application patterns that keep history honest
&lt;/h2&gt;

&lt;p&gt;Keep two clients when history matters: a live URI for writes and hot reads, and a federated URI for legitimate historical access. Time-route in the API: recent windows hit the cluster; older windows accept slower federation. For huge pulls, prefer async export over interactive scans. Set UI expectations honestly ("older than 90 days may load slower"). Users forgive slower history. They do not forgive silent empty results because the app still uses the live URI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Online Archive is not
&lt;/h2&gt;

&lt;p&gt;It is not a backup or point-in-time restore substitute. It is not a free analytics lakehouse for arbitrary heavy joins. It is not writable storage for casual corrections (plan rehydrate flows). It is not worth the complexity on tiny collections. If historical access depends on stages unsupported against archived data, redesign that path before enabling the feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost sanity check
&lt;/h2&gt;

&lt;p&gt;Savings come from smaller disks, smaller backups, and sometimes a lower cluster tier. You pay archive storage and federation processing instead. The win is largest when most bytes are cold and rarely queried.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;estimatedDocumentCount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eligible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;countDocuments&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$lt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cutoff&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${((&lt;/span&gt;&lt;span class="nx"&gt;eligible&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;% eligible`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If most of the collection is eligible and those documents are rarely read, archiving is likely worth it. If eligibility is tiny, you are adding operational surface for theater.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollout checklist and common pitfalls
&lt;/h2&gt;

&lt;p&gt;Confirm tier support and permissions. Measure first-month archive volume. Pilot on a low-risk collection during a quiet window. Prove a known archived id is readable through federation. Monitor job success, lag, and cluster CPU during backlog drain. Document rehydrate steps for the rare "make this hot again" case. Align retention and residency with compliance.&lt;/p&gt;

&lt;p&gt;Common pitfalls stay boring and expensive: archiving data that still changes, missing indexes on rule fields, poor partition choices you cannot cheaply revise, forgetting that the app URI never sees archive rows, using archive where TTL deletion would do, and putting latency-sensitive features on federated reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Online Archive shines when collections are large because of age, not because of an active working set. Define crisp hot windows, archive with indexed predicates, query cold data with slower-SLO honesty, and keep writes on the live cluster. That is how you stop paying premium prices to store silence.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>atlas</category>
      <category>database</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Walking Hierarchies with MongoDB $graphLookup (Without Melting Memory)</title>
      <dc:creator>Davi Orlandi</dc:creator>
      <pubDate>Mon, 05 Oct 2026 14:37:53 +0000</pubDate>
      <link>https://dev.to/dvorlandi/walking-hierarchies-with-mongodb-graphlookup-without-melting-memory-3ejg</link>
      <guid>https://dev.to/dvorlandi/walking-hierarchies-with-mongodb-graphlookup-without-melting-memory-3ejg</guid>
      <description>&lt;p&gt;The first time &lt;code&gt;$graphLookup&lt;/code&gt; melted a primary for me, I was not trying to be clever. We had a category tree. We wanted descendants for an admin screen. Someone omitted &lt;code&gt;maxDepth&lt;/code&gt;, the index story on &lt;code&gt;parentId&lt;/code&gt; was weak, and a catalog import suddenly gave a root tens of thousands of children. The operator did exactly what we asked. We asked for too much on the wrong path. Latency spiked. Memory followed. The personal stake of this essay is that recursive convenience is not free.&lt;/p&gt;

&lt;p&gt;Can MongoDB walk a hierarchy without a closure table? Yes, with guardrails. &lt;code&gt;$graphLookup&lt;/code&gt; recursively follows parent and child links into an &lt;code&gt;as&lt;/code&gt; array. Keep &lt;code&gt;maxDepth&lt;/code&gt;, index &lt;code&gt;connectToField&lt;/code&gt;, prune with &lt;code&gt;restrictSearchWithMatch&lt;/code&gt;, and keep unbounded walks off customer-facing p99 paths. For hot reads on stable trees, materialized paths or closure tables still win.&lt;/p&gt;

&lt;h2&gt;
  
  
  How &lt;code&gt;$graphLookup&lt;/code&gt; behaves when you respect it
&lt;/h2&gt;

&lt;p&gt;For each input document, MongoDB starts from &lt;code&gt;startWith&lt;/code&gt;, matches that value to &lt;code&gt;connectToField&lt;/code&gt; in the &lt;code&gt;from&lt;/code&gt; collection, then repeatedly follows &lt;code&gt;connectFromField&lt;/code&gt; to &lt;code&gt;connectToField&lt;/code&gt; until matches stop or &lt;code&gt;maxDepth&lt;/code&gt; says stop. Results land in the array named by &lt;code&gt;as&lt;/code&gt;. That array is not a guaranteed sorted path. Add &lt;code&gt;depthField&lt;/code&gt; if you need levels.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;$graphLookup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;categories&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;startWith&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;connectFromField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;connectToField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;parentId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;descendants&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxDepth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;depthField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;depth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;restrictSearchWithMatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;
  
  
  A minimal hierarchy that makes the direction obvious
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertMany&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Catalog&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;elec&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Electronics&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;phones&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Phones&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;elec&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;android&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Android&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;phones&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;android&lt;/span&gt;&lt;span class="dl"&gt;"&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="na"&gt;$graphLookup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;categories&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;startWith&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$parentId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;connectFromField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;parentId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;connectToField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ancestors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;depthField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;depth&lt;/span&gt;&lt;span class="dl"&gt;"&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;p&gt;Flip the connect fields (and start from a parent id) to walk downward into descendants. The direction is a modeling choice. The cost is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrails that would have saved that admin screen
&lt;/h2&gt;

&lt;p&gt;Always set &lt;code&gt;maxDepth&lt;/code&gt; when cycles or huge fan-out are possible. Index &lt;code&gt;connectToField&lt;/code&gt; and the fields you prune on. Prune early with &lt;code&gt;restrictSearchWithMatch&lt;/code&gt; (active, tenant, region). Respect memory: wide graphs are hungry, so prefer shallow selective walks. Sort yourself with &lt;code&gt;depthField&lt;/code&gt; plus &lt;code&gt;$sortArray&lt;/code&gt; or client-side ordering. Keep writes simple and reads intentional: parent pointers for writes, &lt;code&gt;$graphLookup&lt;/code&gt; for rare deep views, cache or materialize for hot paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to use it
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Prefer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One level of children&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$lookup&lt;/code&gt; or embedded kids&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frequent ancestor checks on a stable tree&lt;/td&gt;
&lt;td&gt;Materialized path (&lt;code&gt;root/elec/phones&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hot "all descendants" reads, rare reparents&lt;/td&gt;
&lt;td&gt;Closure table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heavy graph algorithms at huge scale&lt;/td&gt;
&lt;td&gt;Specialized graph store&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;android&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;root/elec/phones/android&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That path string looks humble until you realize it turns a recursive read into a prefix query. Humble often wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  A debugging recipe I trust more than optimism
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;explain("executionStats")&lt;/code&gt; on the aggregation. Confirm index use on &lt;code&gt;connectToField&lt;/code&gt;. Measure documents landing in &lt;code&gt;as&lt;/code&gt; for a worst-case seed. Lower depth or tighten the prune filter. Precompute a closure for the hottest roots if the UI must stay interactive. A useful product trick: return one level first and lazy-load children. Most users never need the entire tree in one payload, and most outages start by pretending they do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;$graphLookup&lt;/code&gt; is safe when bounded, indexed, and pruned. It is unsafe as an unbounded "recurse the catalog" button on a hot endpoint. Use it as a sharp tool for admin and ad hoc hierarchy walks, not as a substitute for a read model you should have materialized. The operator is powerful. Power without a depth limit is just a future incident report.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>database</category>
      <category>backend</category>
      <category>performance</category>
    </item>
    <item>
      <title>Lessons I Still Use from MongoDB in Action (Without Rewriting the Book)</title>
      <dc:creator>Davi Orlandi</dc:creator>
      <pubDate>Mon, 05 Oct 2026 14:37:50 +0000</pubDate>
      <link>https://dev.to/dvorlandi/lessons-i-still-use-from-mongodb-in-action-without-rewriting-the-book-52p4</link>
      <guid>https://dev.to/dvorlandi/lessons-i-still-use-from-mongodb-in-action-without-rewriting-the-book-52p4</guid>
      <description>&lt;p&gt;I am not going to retell Kyle Banker's &lt;em&gt;MongoDB in Action&lt;/em&gt; chapter by chapter. Read the book for that. What follows are field notes: the subset of ideas that still change how I model services, especially now that Atlas makes it easy to treat MongoDB like a broken relational database with friendlier braces.&lt;/p&gt;

&lt;p&gt;The personal stake is quieter than an outage story and just as expensive. Most MongoDB pain I see is still mismatched modeling wearing a scaling costume. Teams buy bigger tiers to paper over documents shaped like leftover ER diagrams. These rules of thumb are how I try to stop myself from doing that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 1: Model for queries, not for your ER diagram
&lt;/h2&gt;

&lt;p&gt;Documents are not normalized tables. Start from the questions your API asks. If you always fetch a user with prefs, embedding often wins. If addresses are updated independently and shared, references often win. If you need both, embed a summary and reference the full document.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Embed when the access pattern is one read&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a@b.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;prefs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pt-BR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;marketingOptIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Reference when the child set is unbounded&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cardinality matters. One-to-few, one-to-many, and one-to-squillions deserve different shapes. Huge embedded arrays that grow forever invent document-size pain, and then someone discovers the 16MB limit the hard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 2: Atomicity lives at the document boundary
&lt;/h2&gt;

&lt;p&gt;Single-document updates are atomic. Multi-document workflows need transactions or an outbox you designed on purpose. Prefer invariants that fit in one document. Use multi-document transactions when you must, not on every write out of fear. Pair state changes with outbox rows in the same transaction when you publish events. That last habit has saved me from more "eventual inconsistency" debates than any schema lecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 3: Indexes are part of the schema
&lt;/h2&gt;

&lt;p&gt;Do not treat indexes as a DBA afterthought. Write the filter plus sort shape first, then the index. Avoid an index-per-field shotgun that only creates write amplification. Prefer partial or sparse indexes when only a subset of documents need the path. Watch working set versus RAM. If you cannot read an explain plan on a hot path, you are guessing, and guessing is how "flexible schema" becomes "mysterious latency."&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 4: Aggregation is a product feature
&lt;/h2&gt;

&lt;p&gt;The pipeline is not a party trick. Match early. Project away fat fields before heavy stages. Respect memory limits; spill to disk only when needed and safe. Do not reinvent joins with application-side N+1 if a better schema removes the need. Knowing when not to recurse in the database is modeling maturity, not a lack of ambition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 5: Favor embedding until a compelling reason appears
&lt;/h2&gt;

&lt;p&gt;Useful compelling reasons still look the same years later. The child must stand alone in queries. The array would grow without bound. The read/write ratio makes denormalization too expensive to maintain. Application-level joins are allowed. With correct indexes and projections they are rarely the villain beginners fear. Denormalize only fields that are read often and updated rarely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 6: Flexible schema is not "no schema"
&lt;/h2&gt;

&lt;p&gt;Dumping arbitrary JSON without ownership is an application smell. Share types in the application. Add server-side validation where it pays off. Keep an explicit &lt;code&gt;schemaVersion&lt;/code&gt; when shapes evolve in place. Own field names across services. Flexibility without ownership is just entropy with nicer query syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patterns that still surprise teammates
&lt;/h2&gt;

&lt;p&gt;Cached rollups: store comment counts near the parent instead of counting on every list read. Bucketing: time-series style buckets beat millions of tiny documents when telemetry arrives fast. Hot/cold honesty: separate cold data before the cluster becomes a museum of unread history.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I leave to the book
&lt;/h2&gt;

&lt;p&gt;Deep historical storage-engine lore and exhaustive admin tours belong in the text. The durable lessons I still carry are query-first modeling, index-as-schema, and respect for document atomicity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Design the document around the query and the invariant, not around a relational habit. If that sounds obvious, good. The hard part is keeping the habit when a sprint pressure wants a quick collection "just like the SQL table." The book taught me the vocabulary. Production taught me why the vocabulary matters.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>database</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>Consul Service Discovery That Does Not Lie: Health Checks First</title>
      <dc:creator>Davi Orlandi</dc:creator>
      <pubDate>Mon, 05 Oct 2026 14:37:47 +0000</pubDate>
      <link>https://dev.to/dvorlandi/consul-service-discovery-that-does-not-lie-health-checks-first-13ap</link>
      <guid>https://dev.to/dvorlandi/consul-service-discovery-that-does-not-lie-health-checks-first-13ap</guid>
      <description>&lt;p&gt;Service discovery without honest health checks is a catalog of corpses. I learned that the expensive way: a payments instance that answered &lt;code&gt;200&lt;/code&gt; on &lt;code&gt;/&lt;/code&gt; while its worker pool was deadlocked. Consul kept advertising it. Customers timed out. Dashboards looked fine. The personal stake was simple and humiliating. Discovery had done exactly what we configured it to do. We had configured a lie.&lt;/p&gt;

&lt;p&gt;This essay is about making Consul return instances that can do useful work. The quality of &lt;code&gt;payments.service.consul&lt;/code&gt; is not determined by how pretty the catalog looks. It is determined by how you define healthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registration is a readiness story
&lt;/h2&gt;

&lt;p&gt;Define the service and nested &lt;code&gt;check&lt;/code&gt; blocks (or a &lt;code&gt;checks&lt;/code&gt; array). Register with the local agent. Reload if the service was already registered. Confirm clients filter to the statuses you intend. Initial check status defaults to &lt;code&gt;critical&lt;/code&gt; until the first success. That prevents newborns from receiving traffic before they are ready. Override with &lt;code&gt;status = "passing"&lt;/code&gt; only when you understand the trade, because starting green is how you advertise an unfinished boot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer HTTP and TCP checks over shell folklore
&lt;/h2&gt;

&lt;p&gt;If you are curling an endpoint from a script check, use an HTTP check. If you are probing a port with netcat, use a TCP check. Native checks avoid shell sprawl and reduce remote-execution risk compared with broadly enabled script checks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"payments"&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;

  &lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"payments-http"&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP /health/ready"&lt;/span&gt;
    &lt;span class="nx"&gt;http&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://127.0.0.1:8080/health/ready"&lt;/span&gt;
    &lt;span class="nx"&gt;method&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GET"&lt;/span&gt;
    &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10s"&lt;/span&gt;
    &lt;span class="nx"&gt;timeout&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2s"&lt;/span&gt;
    &lt;span class="nx"&gt;failures_before_critical&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&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;Probe &lt;code&gt;127.0.0.1&lt;/code&gt; so you evaluate the local process, not a load balancer illusion. Keep timeouts short. Use failure thresholds to avoid removing capacity on a single blip. Consul's HTTP mapping is worth memorizing: &lt;code&gt;2xx&lt;/code&gt; is passing, &lt;code&gt;429&lt;/code&gt; is warning, anything else is critical.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"payments-tcp"&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"TCP 8080"&lt;/span&gt;
  &lt;span class="nx"&gt;tcp&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"127.0.0.1:8080"&lt;/span&gt;
  &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10s"&lt;/span&gt;
  &lt;span class="nx"&gt;timeout&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1s"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Design &lt;code&gt;/health&lt;/code&gt; on purpose
&lt;/h2&gt;

&lt;p&gt;Separate concerns before you wire Consul to them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Typical Consul use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/health/live&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Process is up&lt;/td&gt;
&lt;td&gt;Orchestrator restarts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/health/ready&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Can accept traffic&lt;/td&gt;
&lt;td&gt;Consul check target&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Avoid marking every instance critical when a shared database hiccups unless evacuating everyone is truly safer than serving degraded. Mass critical status amplifies outages. Readiness should prove the instance can accept work: for example a cheap in-process slot acquire with a timeout that returns 503 when saturated. Liveness should answer a smaller question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other check types and when they earn their complexity
&lt;/h2&gt;

&lt;p&gt;TTL checks let the application heartbeat Consul. Useful when only the app knows readiness. Missed heartbeat becomes critical. gRPC checks fit services that expose the standard health protocol. Alias checks mirror another service or node health for sidecars. Script checks are powerful and dangerous; prefer &lt;code&gt;enable_local_script_checks&lt;/code&gt; over broadly enabling remote script checks.&lt;/p&gt;

&lt;p&gt;You may attach several checks to one service. Keep the set small. Three flaky checks create a jittery pool that looks like network chaos and trains operators to ignore status changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client discovery only becomes trustworthy after checks are honest
&lt;/h2&gt;

&lt;p&gt;After checks tell the truth, clients can resolve Consul DNS with local caching, use health HTTP APIs with blocking queries, or use Connect when you need mTLS and intentions. DNS detail matters: by default warning statuses may still appear in DNS answers. Set &lt;code&gt;dns_config.only_passing = true&lt;/code&gt; (or filter explicitly in the HTTP API) when only passing instances should receive traffic. I have seen teams "debug networking" for hours when the real bug was DNS still answering for warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registration hygiene that keeps the catalog adult
&lt;/h2&gt;

&lt;p&gt;Give each instance a unique &lt;code&gt;id&lt;/code&gt; (&lt;code&gt;payments-i-0abc&lt;/code&gt;). Tag for version or canary. Deregister on shutdown, or keep intervals short enough that dead boxes disappear. Enable maintenance mode before disruptive node work so discovery stops advertising without pretending the process crashed. Pair Consul readiness with orchestrator readiness gates during deploys so both systems tell the same story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Consul discovery is only as trustworthy as your checks. Prefer native HTTP, TCP, or gRPC probes. Separate liveness from readiness. Configure DNS and API clients to honor the statuses you actually want. Healthy discovery is less about the catalog and more about refusing to advertise lies. The day I stopped pointing readiness at a decorative &lt;code&gt;/&lt;/code&gt; endpoint was the day discovery started feeling like infrastructure again instead of theater.&lt;/p&gt;

</description>
      <category>consul</category>
      <category>devops</category>
      <category>microservices</category>
      <category>sre</category>
    </item>
    <item>
      <title>Go Test Coverage Is a Smoke Alarm, Not a Quality Certificate</title>
      <dc:creator>Davi Orlandi</dc:creator>
      <pubDate>Mon, 05 Oct 2026 14:37:43 +0000</pubDate>
      <link>https://dev.to/dvorlandi/go-test-coverage-is-a-smoke-alarm-not-a-quality-certificate-2ik6</link>
      <guid>https://dev.to/dvorlandi/go-test-coverage-is-a-smoke-alarm-not-a-quality-certificate-2ik6</guid>
      <description>&lt;p&gt;I used to feel proud when a Go service crossed a high coverage number. Pull requests celebrated &lt;code&gt;87.3%&lt;/code&gt;. Gates refused merges below &lt;code&gt;80%&lt;/code&gt;. The bar looked meaningful. Then production taught me what the metric never promised: that executed lines are not the same as protected behavior.&lt;/p&gt;

&lt;p&gt;The failure that stuck with me was not exotic. A repeated request with the same idempotency key created two payment attempts. The suite had touched the code. Coverage looked healthy. The business rule that mattered most had never been asserted. That is the personal stake behind this essay. Coverage can tell you which statements ran. It cannot tell you whether the tests understood the risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deceptive green
&lt;/h2&gt;

&lt;p&gt;Coverage tells you which statements ran. It does not tell you whether assertions were meaningful, whether races exist, or whether the API still matches the product.&lt;/p&gt;

&lt;p&gt;Consider a tiny function with two independent conditions. One carefully chosen input can execute every line while leaving whole combinations of behavior untested. Statement coverage reports 100%. Path coverage would still be incomplete. Go's default tooling measures statements. That is useful. It is not a proof.&lt;/p&gt;

&lt;p&gt;Worse: a test that calls a function and ignores the result still paints lines green. Happy-path suites that never touch &lt;code&gt;err != nil&lt;/code&gt; branches look healthy until production finds the branch. So when someone asks for an "80% gate," ask a follow-up: 80% of what risk?&lt;/p&gt;

&lt;h2&gt;
  
  
  What coverage is good for
&lt;/h2&gt;

&lt;p&gt;Coverage earns its keep as a flashlight. It finds untouched packages before a release. It spots dead error paths nobody exercises. It guides "what should I test next?" during a refactor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;test&lt;/span&gt; ./... &lt;span class="nt"&gt;-coverprofile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cover.out
go tool cover &lt;span class="nt"&gt;-html&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cover.out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the HTML report. Hunt red in money, auth, and data-loss code. Ignore vanity in &lt;code&gt;main&lt;/code&gt; wiring and generated protobufs. The value is not the percentage at the top. The value is which dangerous lines never ran.&lt;/p&gt;

&lt;h2&gt;
  
  
  What coverage lies about
&lt;/h2&gt;

&lt;p&gt;Assertion-free tests are theater: execution without checks. Happy-path bias is fragile: high coverage with zero failure cases. Concurrency is invisible: coverage does not prove race freedom; &lt;code&gt;-race&lt;/code&gt; does. Integration reality still bites: mocks can give 100% while SQL breaks in staging.&lt;/p&gt;

&lt;p&gt;I have seen projects with 95% coverage that still ship production bugs in the untested 5% of error handling. I have also seen quieter suites near 40% that felt more trustworthy because every tested line mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table-driven tests beat a percentage
&lt;/h2&gt;

&lt;p&gt;If you copy-paste cases, you probably want a table. Tables amortize careful assertions and make gaps obvious.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestParseDuration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
        &lt;span class="n"&gt;in&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt;
        &lt;span class="n"&gt;want&lt;/span&gt;    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;
        &lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;}{&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"seconds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"30s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"invalid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"abc"&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="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"empty"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&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="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"negative"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-1s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&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="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ParseDuration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wantErr&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"expected error for %q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unexpected err: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"got %v want %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want&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="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;Use &lt;code&gt;t.Run&lt;/code&gt; so failures name the case. Keep error-path tables separate when assertion logic diverges. Capture loop variables correctly if you mark subtests parallel, and do not share mutable state across parallel cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  A workflow without the cult
&lt;/h2&gt;

&lt;p&gt;Write behavior tests first, edges included. Implement until they pass. Run coverage and only add tests for surprising red in risky code. Stop when residual red is glue or unreachable defense-in-depth. Chasing the last five percent often couples tests to implementation details. Those tests punish refactors without catching bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommendations I actually follow
&lt;/h2&gt;

&lt;p&gt;Identify critical packages. Business logic yes. Logger setup and server wiring usually no (cover them in integration). Aim high on the testable core, but do not worship 100%. Prefer risk conversations over global gates. Soft floors on business packages beat a blanket 95% rule that invents empty tests. Do not treat "coverage must never decrease" as sacred. Sometimes deleting bad tests is progress. Unit coverage is never enough: integration, contract tests, fuzzing, and &lt;code&gt;-race&lt;/code&gt; catch classes of failure a percentage will not mention.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go test ./... -race -count=1&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go test ./... -coverprofile=cover.out -covermode=atomic&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go tool cover -func=cover.out | tee coverage.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fail builds for selected packages when coverage drops and the diff touches those packages. Blanket gates encourage people to delete hard-to-cover defenses or add hollow tests. Also treat flaky tests as defects. A suite that is green nine times out of ten trains the team to ignore red builds, which destroys more quality than a low number ever will.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Until critical code is covered, you definitely have untested risk. Once it is covered, you might still have untested risk. Treat coverage as a smoke alarm, not a certificate. Shine it on dangerous packages, invest in sharp table-driven cases, and keep race detection in CI. Good Go testing is not about making a report look impressive. It is about enough confidence that the important behaviors survive real traffic, repeated requests, and the strange timing problems that only appear after deploy.&lt;/p&gt;

</description>
      <category>go</category>
      <category>testing</category>
      <category>qualityassurance</category>
      <category>devops</category>
    </item>
    <item>
      <title>Rate Limiting Is Not Back Pressure: Layering Protection in Go Services</title>
      <dc:creator>Davi Orlandi</dc:creator>
      <pubDate>Mon, 05 Oct 2026 14:37:39 +0000</pubDate>
      <link>https://dev.to/dvorlandi/rate-limiting-is-not-back-pressure-layering-protection-in-go-services-12p</link>
      <guid>https://dev.to/dvorlandi/rate-limiting-is-not-back-pressure-layering-protection-in-go-services-12p</guid>
      <description>&lt;p&gt;I used to treat rate limiting and back pressure as synonyms people swapped in design reviews for variety. Then an afternoon that should have been quiet melted a service I cared about, and the difference stopped being academic.&lt;/p&gt;

&lt;p&gt;We had an ingestion API capped at 5,000 requests per second. That number came from an old benchmark with margin. A migration slowed the database from roughly 4ms writes to 90ms. The rate limiter kept admitting 5,000 rps because nothing about the database was visible to it. The internal queue between HTTP and the writer grew. Latency climbed from tens of milliseconds to tens of seconds. The pod died on memory. The rate limiter did its job. Its job was never this.&lt;/p&gt;

&lt;p&gt;That story is why I write this essay. Rate limiting enforces a fixed ceiling chosen in advance. Back pressure is a signal travelling backwards from the component that is actually struggling. Load shedding is what you do when acceptance would make everything worse. The thing that defeats all three is an unbounded queue: it converts a fast rejection into unbounded latency and then a memory failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two mechanisms, two failures
&lt;/h2&gt;

&lt;p&gt;Rate limiting answers: is this caller allowed to send this much? The number comes from a contract, a tier, or last year's load test.&lt;/p&gt;

&lt;p&gt;Back pressure answers: can this system absorb more right now? The answer moves when a dependency slows, a node disappears, or a noisy neighbor steals CPU.&lt;/p&gt;

&lt;p&gt;The direction of the arrow is the whole distinction. Rate limiting pushes a constraint at the entrance. Back pressure pushes information upstream from wherever the bottleneck currently lives. You usually want both. Pretending one replaces the other is how you get green policy dashboards next to a dying process.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unbounded queue is what makes overload fatal
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// unbounded in practice if you keep spawning senders&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ErrOverloaded&lt;/span&gt; &lt;span class="c"&gt;// only works if the channel is buffered and full&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// capacity is an explicit decision&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An unbounded buffer does not remove overload. It hides it until latency and memory become the outage. Export queue depth everywhere you have a queue. Depth usually moves before the error rate does. That single habit has saved me more times than any clever token-bucket tweak.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical layering order in Go
&lt;/h2&gt;

&lt;p&gt;At the HTTP edge I want a boring sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify the caller (limits need a key)&lt;/li&gt;
&lt;li&gt;Rate limit per API key, IP, or tenant&lt;/li&gt;
&lt;li&gt;Cap in-flight work with a semaphore or buffered channel&lt;/li&gt;
&lt;li&gt;Run the handler with deadlines&lt;/li&gt;
&lt;li&gt;Bound concurrency on outbound clients too
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Gate&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;lim&lt;/span&gt;   &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Limiter&lt;/span&gt;
    &lt;span class="n"&gt;slots&lt;/span&gt; &lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Gate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Allow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"rate limit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusTooManyRequests&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;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slots&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}{}&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slots&lt;/span&gt; &lt;span class="p"&gt;}()&lt;/span&gt;
            &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServeHTTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Retry-After"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"overloaded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusServiceUnavailable&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;strong&gt;429&lt;/strong&gt; for policy. Use &lt;strong&gt;503&lt;/strong&gt; for overload. Clients should treat them differently: 429 means slow to the agreed rate; 503 means back off harder, with jitter, and consider opening a circuit. Collapsing both into one generic "retry later" response trains clients to stampede.&lt;/p&gt;

&lt;h2&gt;
  
  
  Load shedding is back pressure with a decision attached
&lt;/h2&gt;

&lt;p&gt;Reject at the entrance before work starts. Prefer dropping low-regret traffic first: batch jobs, scrapers, expensive exports. Protect login and checkout longer. Check deadlines on queued work so you do not spend capacity on results nobody will read. Shedding is not cruelty. It is choosing which failures you can explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I look for in reviews
&lt;/h2&gt;

&lt;p&gt;Every queue should have a bound and a full policy. 429 and 503 should be intentional, not interchangeable. Shed rate should be on a dashboard, not only error rate. Retries should be budgeted at each layer. Clients should honor &lt;code&gt;Retry-After&lt;/code&gt;. If those five lines are missing, the service is polite until it is catastrophic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Rate limiting is a configured ceiling. Back pressure is a live capacity signal. One encodes what you believed last year. The other encodes what is true this minute. Bound every queue, fail fast when full, layer a token bucket in front of a concurrency gate, and teach clients to listen. That is not a research project. It is the difference between a graceful slow-down and a chain-reaction outage that starts with a number you once thought was safe.&lt;/p&gt;

</description>
      <category>go</category>
      <category>backend</category>
      <category>performance</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Bloom Filters for Username Checks: The Case Study (and When to Walk Away)</title>
      <dc:creator>Davi Orlandi</dc:creator>
      <pubDate>Mon, 05 Oct 2026 14:37:36 +0000</pubDate>
      <link>https://dev.to/dvorlandi/bloom-filters-for-username-checks-the-case-study-and-when-to-walk-away-3o6i</link>
      <guid>https://dev.to/dvorlandi/bloom-filters-for-username-checks-the-case-study-and-when-to-walk-away-3o6i</guid>
      <description>&lt;p&gt;The signup form that taught me to care about Bloom filters was not glamorous. People typed. We checked. Most answers were "available." Indexed database lookups are cheap one at a time. They are not free when every keystroke across a growing product becomes a read against the authoritative store. At some point the question stops being "can the database answer?" and becomes "should the database be the first system we ask every time?"&lt;/p&gt;

&lt;p&gt;A Bloom filter sits in front of that path as a fast pre-check. It does not replace the database. It does not replace a &lt;code&gt;UNIQUE&lt;/code&gt; constraint. It answers one useful question quickly: is this username definitely not in the set? If the filter says absent, skip the read. If it says maybe, confirm with the source of truth.&lt;/p&gt;

&lt;p&gt;The personal stake is trust. Get the semantics wrong and you block free names, open ticket storms, and teach users that your product lies about availability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why direct username checks get expensive
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caches help for popular names. They help less for random new strings nobody has typed before. That is the awkward traffic mix of a signup box: mostly negatives, often unique, always latency-sensitive in the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the filter actually promises
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Filter result&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Next step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;td&gt;Definitely not inserted&lt;/td&gt;
&lt;td&gt;Treat as available for the read path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Present&lt;/td&gt;
&lt;td&gt;Probably inserted&lt;/td&gt;
&lt;td&gt;Confirm with the database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False positive&lt;/td&gt;
&lt;td&gt;Present in filter, absent in DB&lt;/td&gt;
&lt;td&gt;One extra lookup (acceptable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False negative for inserted values&lt;/td&gt;
&lt;td&gt;Should not happen if the filter was updated&lt;/td&gt;
&lt;td&gt;Almost always means the filter is stale&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;False positives are the trade. For username checks they are cheap. False negatives for names you already inserted are not acceptable on this path, which is why the write path must keep the filter honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the availability check as a story, not a party trick
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Signup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&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;username&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;maybe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bloom&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UsernameFree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// Redis down → DB, never invent availability&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;maybe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UsernameFree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&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;On successful registration the sequence matters more than the data structure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;INSERT&lt;/code&gt; with a &lt;strong&gt;UNIQUE&lt;/strong&gt; constraint (the real guardian)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BF.ADD&lt;/code&gt; the username, or emit an event a worker applies&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Skip step 2 and the filter drifts. Drift raises false positives (extra database hits). It does not invent false negatives for names that were correctly added earlier, unless you never added them at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that looks like flaky signup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if bloom.Exists(username) {
    return "already taken"  // wrong
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A false positive now blocks a free name. Users open tickets. Logs show Bloom hits with no matching row. Trust evaporates. The rule I write in every review is blunt: a Bloom "present" is a hint, never a verdict. The insert's unique constraint still prevents races when two clients check the same free name at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production considerations that decide whether this stays elegant
&lt;/h2&gt;

&lt;p&gt;Keep the database as authority. The filter speeds reads. The database decides writes. Always.&lt;/p&gt;

&lt;p&gt;Keep the filter synchronized. Update on every successful signup. Rebuild periodically from a snapshot into a new key (&lt;code&gt;bf:usernames:v2&lt;/code&gt;), dual-write briefly, then flip readers.&lt;/p&gt;

&lt;p&gt;Plan for deletes and renames. Classic Bloom filters do not delete cleanly. Counting Bloom or Cuckoo filters exist, but complexity jumps. If renames are common, budget rebuilds or pick another structure.&lt;/p&gt;

&lt;p&gt;Size for tomorrow. Estimate cardinality 18 to 24 months out. Pick a target false-positive rate (around 1% is common for pre-checks). Alert when fill factor climbs. Undersizing quietly kills the optimization as the fallback rate rises.&lt;/p&gt;

&lt;p&gt;Normalize once. Lowercase, trim, and apply the same Unicode normalization everywhere. A filter cannot fix inconsistent identity rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you should walk away
&lt;/h2&gt;

&lt;p&gt;Walk away when exact answers are the product and there is no cheap confirmation path. Walk away when deletes dominate and rebuilds are not operationally owned. Walk away when the set is small enough that a Redis &lt;code&gt;SET&lt;/code&gt; or the database alone is fine. Walk away when nobody owns metrics, rebuilds, or cutovers. Walk away when a false positive is user-visible and expensive, such as checkout lockouts or access denials.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Use a Bloom filter as a performance shield in front of a unique constraint, never as the authority. For username checks, "filter fast, confirm with DB" is the whole architecture. If your case cannot tolerate that confirmation step, choose a different tool. The structure is clever. Cleverness is not the same as correctness under concurrency.&lt;/p&gt;

</description>
      <category>bloomfilter</category>
      <category>redis</category>
      <category>backend</category>
      <category>performance</category>
    </item>
    <item>
      <title>Watermill Pitfalls I Wish I Knew Before Shipping EDA in Go</title>
      <dc:creator>Davi Orlandi</dc:creator>
      <pubDate>Mon, 05 Oct 2026 14:37:33 +0000</pubDate>
      <link>https://dev.to/dvorlandi/watermill-pitfalls-i-wish-i-knew-before-shipping-eda-in-go-15lf</link>
      <guid>https://dev.to/dvorlandi/watermill-pitfalls-i-wish-i-knew-before-shipping-eda-in-go-15lf</guid>
      <description>&lt;p&gt;Watermill made event-driven Go feel almost as ergonomic as an HTTP router. That comfort is a gift. It is also how I talked myself into shipping consistency bugs that did not show up until a quiet Sunday night, when a DNS blip and a partial deploy collided.&lt;/p&gt;

&lt;p&gt;I already wrote about getting started elsewhere. This essay is about the physics that remain after the boilerplate disappears. Dual writes. Ordering surprises inside handler groups. Duplicate delivery that everyone "knows about" until money is involved. Middleware that looks complete until one poison payload pins a consumer forever.&lt;/p&gt;

&lt;p&gt;The personal stake is simple. Event-driven architecture does not fail loudly the way a 500 does. It fails by drifting: the database believes something happened, and the rest of the world never heard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The outage that is not a bug
&lt;/h2&gt;

&lt;p&gt;A pod eviction. A rebalance. A process stopped between two steps that both needed to happen. The program did not "have a bug" in the usual sense. The execution simply ended. Durable design accepts interruptions and recovers to a correct state anyway. Watermill will not invent that durability for you. It will make durable patterns easier to express once you decide they matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publish inside the same transaction, or admit you did not
&lt;/h2&gt;

&lt;p&gt;The classic race looks innocent.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Insert the order in Postgres&lt;/li&gt;
&lt;li&gt;Publish &lt;code&gt;OrderCreated&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Crash between 1 and 2&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The database believes the order exists. Downstream services never saw the fact. Support tickets arrive days later, and the on-call engineer stares at two systems that are both "correct" in isolation.&lt;/p&gt;

&lt;p&gt;Watermill's Forwarder (outbox) pattern exists for this. Write domain state and the outbound message in one database transaction. A Forwarder process later moves outbox rows to Kafka, Redis Streams, or whatever broker you chose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Command handler
  └─ BEGIN
       ├─ mutate domain tables
       └─ insert outbox row
     COMMIT
Forwarder
  └─ read outbox → publish → mark forwarded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you skip the outbox, document the publish as best-effort and design compensations on purpose. Silent dual-write is how event-driven architecture earns a bad name inside a company that then refuses to try again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handler groups change the meaning of "this message"
&lt;/h2&gt;

&lt;p&gt;By default, CQRS-style processors often create one subscriber per handler. That is fine when each topic carries one event type. When many event types share a topic and order matters, handler groups put those handlers behind one subscriber so the stream is processed sequentially.&lt;/p&gt;

&lt;p&gt;The sharp edge caught me later than it should have. If handler A succeeds and handler B fails inside the same group, redelivery runs A again. So keep one handler per event type per group when you can, make every handler idempotent anyway, and treat &lt;code&gt;AckOnUnknownEvent&lt;/code&gt; as a loaded gun. Silence is not the same as correctness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exactly-once is a wish; idempotency is a design
&lt;/h2&gt;

&lt;p&gt;Most brokers Watermill wraps deliver at-least-once. Rebalances, retries, and partial failures will replay messages. Plan for it on day one, not after the first double charge.&lt;/p&gt;

&lt;p&gt;Practical options that have held up for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store &lt;code&gt;message.UUID&lt;/code&gt; or a business key in a processed-events table with a unique index&lt;/li&gt;
&lt;li&gt;Prefer naturally idempotent writes (&lt;code&gt;UPSERT&lt;/code&gt;, conditional updates)&lt;/li&gt;
&lt;li&gt;Use a short TTL cache only for soft dedupe, never for money
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;MarkPaid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&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;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderPaid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dedupe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EventID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarkPaid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderID&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;
  
  
  Retries without a poison path are optional self-harm
&lt;/h2&gt;

&lt;p&gt;Retry middleware is mandatory. Infinite retries without a dead-letter path are how one bad payload pins a consumer forever. Wire three layers: backoff retries for transient errors, poison or dead-letter after a capped budget, and a circuit breaker in front of fragile dependencies.&lt;/p&gt;

&lt;p&gt;Separate handler bugs from dependency outages. A nil pointer should not share the same budget as a 503 from payments. Alert on dead-letter depth the moment the first message lands. A quiet DLQ is a forgotten one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request-reply is not your default hop
&lt;/h2&gt;

&lt;p&gt;Watermill supports request-reply. Used everywhere, it reintroduces the coupling events were meant to remove. Prefer async facts for things that already happened, and plain RPC when you truly need an immediate answer. Keep request-reply for the rare case where a client is waiting and the command must be processed now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topics that mean everything age badly
&lt;/h2&gt;

&lt;p&gt;One &lt;code&gt;domain-events&lt;/code&gt; topic with forty payload types feels convenient in week one. By month three it becomes an ordering, ACL, and schema-evolution mess. Prefer one aggregate type or bounded context per topic family. Version payloads deliberately. Pass correlation and causation ids in metadata so a cascade is greppable when someone asks "what started this?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove the hard parts
&lt;/h2&gt;

&lt;p&gt;Before calling a service done, I want a short checklist that is not theater:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State and event publish are atomic (outbox) or explicitly best-effort&lt;/li&gt;
&lt;li&gt;Handlers survive duplicate delivery&lt;/li&gt;
&lt;li&gt;Retry plus dead-letter are configured and alerted&lt;/li&gt;
&lt;li&gt;Ordering needs map to subscriber topology&lt;/li&gt;
&lt;li&gt;Lag, error class, and DLQ depth are on a dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A surprising number of "Watermill bugs" are schema bugs wearing a messaging costume. Add fields carefully. Keep consumers tolerant of unknowns. Contract-test critical events in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Watermill removes boilerplate. It does not remove distributed-systems physics. Treat the outbox as default for anything money-adjacent, design for at-least-once, and be intentional about handler groups. The library will happily help you build a resilient backbone, or a very fast way to spread inconsistency. The difference is almost never the import path. It is whether you respected the gaps between steps.&lt;/p&gt;

</description>
      <category>go</category>
      <category>eventdriven</category>
      <category>watermill</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Redis Queues in Practice: Lists, Streams, and When Each One Wins</title>
      <dc:creator>Davi Orlandi</dc:creator>
      <pubDate>Mon, 05 Oct 2026 14:37:30 +0000</pubDate>
      <link>https://dev.to/dvorlandi/redis-queues-in-practice-lists-streams-and-when-each-one-wins-mm6</link>
      <guid>https://dev.to/dvorlandi/redis-queues-in-practice-lists-streams-and-when-each-one-wins-mm6</guid>
      <description>&lt;p&gt;The first Redis queue I shipped looked almost too good to be true. We already had Redis for sessions. Someone pasted two commands into a design doc: &lt;code&gt;LPUSH&lt;/code&gt; on the producer, &lt;code&gt;BRPOP&lt;/code&gt; on the worker. Reviewers nodded. The first week felt like a small miracle. Jobs flowed. Dashboards stayed green. I remember telling a teammate that we had "solved" background work without introducing another broker.&lt;/p&gt;

&lt;p&gt;Then a worker pod got OOM-killed mid-send. Redis had already removed the job from the list. The email never went out. Nobody noticed until a customer asked, days later, why the receipt never arrived. Over the next month we found a thin trail of vanished reconciliations: not catastrophic, just enough to make me distrust every green latency chart.&lt;/p&gt;

&lt;p&gt;That is the personal stake behind this essay. Lists are wonderful and Lists are incomplete. Streams are more ceremony and Streams remember what you asked them to remember. Choosing between them is less about microbenchmarks and more about whether you can sleep when a pod dies at the wrong millisecond.&lt;/p&gt;

&lt;h2&gt;
  
  
  What BRPOP actually promises
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;BRPOP&lt;/code&gt; is honest about one thing: it removes the element and returns it in a single step. After that handoff, the job lives only in the worker's memory. If the process dies before the side effect finishes, Redis has already forgotten the work. That is at-most-once delivery. It is fine for warming a cache. It is the wrong contract for billing, outbound email, or anything a human will notice missing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BRPop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"jobs:email"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&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;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&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 class="c"&gt;// die here and the job is gone&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used to treat that snippet as "simple Redis." It is simple. It is also a silent hole in the floor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making Lists honest without leaving Redis
&lt;/h2&gt;

&lt;p&gt;You can keep Lists and still recover from crashes. The pattern is familiar once you have lived through the first lost job: move each item atomically into a processing list, finish the side effect, then remove it. A reaper returns stale processing entries when a worker disappears.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;moved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BLMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"jobs:email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"jobs:email:processing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"RIGHT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"LEFT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&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;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;moved&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="c"&gt;// leave it for the reaper, or push back to ready&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LRem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"jobs:email:processing"&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 class="n"&gt;moved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works. You now own heartbeats, per-worker keys, reaper timing, and awkward edge cases like identical payloads (&lt;code&gt;LREM&lt;/code&gt; matches by value, so every job needs a unique id). For a single service with a moderate backlog and one worker pool, that trade can be rational. You stay on familiar commands. You accept that reliability is application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streams as bookkeeping you stop reinventing
&lt;/h2&gt;

&lt;p&gt;Streams change the story. &lt;code&gt;XADD&lt;/code&gt; appends. A consumer group tracks deliveries in the Pending Entries List. &lt;code&gt;XACK&lt;/code&gt; clears a pending entry. &lt;code&gt;XAUTOCLAIM&lt;/code&gt; reassigns work that sat idle too long. The crash recovery you would otherwise write by hand lives closer to Redis itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XReadGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XReadGroupArgs&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Group&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;"workers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Consumer&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"worker-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Streams&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"jobs:email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Block&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&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;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;streams&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Messages&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt; &lt;span class="c"&gt;// no XACK; reclaim will retry&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XAck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"jobs:email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"workers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&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;The production habits that matter are not exotic. Cap retries, then copy poison messages to a dead-letter stream. Treat delivery as at-least-once and make handlers tolerate duplicates. Trim with &lt;code&gt;MAXLEN ~&lt;/code&gt; so the stream cannot grow forever. Alert on PEL size and oldest pending idle time. Those are the difference between "we use Streams" and "we understand Streams."&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison that actually decides the rewrite
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;List + BLMOVE + reaper&lt;/th&gt;
&lt;th&gt;Stream + consumer group&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Crash recovery&lt;/td&gt;
&lt;td&gt;You build it&lt;/td&gt;
&lt;td&gt;PEL + &lt;code&gt;XAUTOCLAIM&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-flight visibility&lt;/td&gt;
&lt;td&gt;Scan processing keys&lt;/td&gt;
&lt;td&gt;&lt;code&gt;XPENDING&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replay / audit&lt;/td&gt;
&lt;td&gt;Gone after ack&lt;/td&gt;
&lt;td&gt;Until trimmed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple consumer groups&lt;/td&gt;
&lt;td&gt;Awkward fan-out copies&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ops complexity&lt;/td&gt;
&lt;td&gt;Low until the reaper&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Throughput for small payloads is usually similar. Redis's single-threaded command path dominates. The decision is correctness tooling, not microseconds. If two subsystems need the same message independently, Streams with separate groups win immediately. If a worker crash after receive is acceptable, Lists may still be enough. If Redis is already the shared critical path for everything in the company, consider whether messaging deserves a dedicated broker while Redis stays on cache and sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas I keep writing down
&lt;/h2&gt;

&lt;p&gt;Trimming unacked stream entries with &lt;code&gt;MAXLEN&lt;/code&gt; follows position, not acknowledgement. Size the bound above your largest backlog. Ack after the durable side effect, never before. Prefer stable consumer names; pod names as consumer ids litter the group until someone cleans them up. And please do not use Pub/Sub as a job queue. Offline consumers lose messages. Live fan-out is a different problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;I still reach for Lists when the queue is small, owned by one service, and the team understands the processing-list pattern. I reach for Streams when disappearance is unacceptable, when multiple groups need the same log, or when I am tired of reinventing the reaper. The painful lesson from that first lost receipt was not "Redis is bad at queues." It was that a queue without acknowledgement is a delayed apology waiting for the right crash.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>backend</category>
      <category>queues</category>
      <category>go</category>
    </item>
    <item>
      <title>How I Use SDD (Spec-Driven Development)</title>
      <dc:creator>Davi Orlandi</dc:creator>
      <pubDate>Mon, 05 Oct 2026 13:17:04 +0000</pubDate>
      <link>https://dev.to/dvorlandi/how-i-use-sdd-spec-driven-development-2g9g</link>
      <guid>https://dev.to/dvorlandi/how-i-use-sdd-spec-driven-development-2g9g</guid>
      <description>&lt;p&gt;If you follow tech the way I do, you've probably already felt lost with the flood of innovations launching every week since AI took off (or at least opened Twitter/LinkedIn and thought "okay, I became obsolete in 3 days"). Today I'll share a bit of my experience with one of them: Spec-Driven Development.&lt;/p&gt;

&lt;h1&gt;
  
  
  What is SDD?
&lt;/h1&gt;

&lt;p&gt;SDD, or Spec-Driven Development, is a software development framework where the &lt;strong&gt;specification comes before the code&lt;/strong&gt;. Instead of generating code unchecked, we define functional and technical specifications that guide development, serve as documentation and history, and help enrich the context LLMs use throughout the process.&lt;/p&gt;

&lt;p&gt;There are many ways to use SDD. You can rely on ready-made setups like &lt;a href="https://github.com/gotalab/cc-sdd" rel="noopener noreferrer"&gt;cc-sdd&lt;/a&gt; for Claude Code, or &lt;a href="https://github.com/madebyaris/spec-kit-command-cursor" rel="noopener noreferrer"&gt;Spec Kit Command&lt;/a&gt; for Cursor, which already ship with ready-to-use commands. You can also build your own commands on top of these setups to fit your day-to-day workflow. It's common to have agents, skills, or commands specialized for each company's workflows — "How to deploy the backend", "Security acceptance criteria", and so on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow
&lt;/h2&gt;

&lt;p&gt;SDD implementations vary by use case, but most go through these phases:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feeuyo5hgq0b2yomvcgyk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feeuyo5hgq0b2yomvcgyk.png" alt=" " width="800" height="205"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Functional Spec
&lt;/h3&gt;

&lt;p&gt;This is where we describe requirements functionally. Use as little technical language as possible (yes, that's harder than it sounds).&lt;/p&gt;

&lt;p&gt;You can start from an idea, a task from tools like Jira via MCP, or even a bug. Example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Users complete onboarding but receive no follow-up. That reduces engagement and doesn't guide them through the next steps inside the platform.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Since this is a discovery moment, the AI starts asking questions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AI: Which users should we implement this for?&lt;/p&gt;

&lt;p&gt;You: All newly registered users who finished onboarding.&lt;/p&gt;

&lt;p&gt;AI: Exactly when should the email be sent?&lt;/p&gt;

&lt;p&gt;You: Immediately after confirming that onboarding completed successfully.&lt;/p&gt;

&lt;p&gt;AI: What are the functional requirements?&lt;/p&gt;

&lt;p&gt;You:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detect the moment onboarding completes&lt;/li&gt;
&lt;li&gt;Trigger the email only once per user&lt;/li&gt;
&lt;li&gt;Personalize the email with basic data (name, etc.)&lt;/li&gt;
&lt;li&gt;Implement retry if the email service fails&lt;/li&gt;
&lt;li&gt;Log successes and errors&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;These refinements produce a Functional Spec — everything needed to describe the technical details of the implementation.&lt;/p&gt;

&lt;p&gt;That document should also include clear acceptance criteria, which serve as the validation baseline throughout the flow.&lt;/p&gt;

&lt;p&gt;The spec must be editable and have an approval mechanism. In almost every case, you'll iterate with the LLM until you land on something satisfactory.&lt;/p&gt;

&lt;p&gt;This phase can be started with commands like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/sdd.spec functional Envio de email de onboarding&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Some setups use flags such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--clarify&lt;/code&gt; when something isn't clear&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--approve&lt;/code&gt; when you consider the spec ready&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Technical Spec
&lt;/h3&gt;

&lt;p&gt;With the functional specification defined, translate the business requirements into technical details. You can start with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/sdd.spec technical&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AI: What technology stack will you use?&lt;/p&gt;

&lt;p&gt;You: Node.js with Express for the service, integration with an email provider (e.g. SES or similar), and a queue (e.g. SQS or PubSub) for asynchronous processing.&lt;/p&gt;

&lt;p&gt;AI: What API design approach?&lt;/p&gt;

&lt;p&gt;You: Event-driven architecture. The trigger will be based on the &lt;code&gt;user.onboarding.completed&lt;/code&gt; event, with asynchronous communication.&lt;/p&gt;

&lt;p&gt;AI: Which services or platforms will be integrated?&lt;/p&gt;

&lt;p&gt;You: An email service for sending, a messaging system for the queue, and possibly a template service. Also integration with the auth system to keep data consistent.&lt;/p&gt;

&lt;p&gt;AI: What are the performance requirements?&lt;/p&gt;

&lt;p&gt;You:&lt;br&gt;
Event processing latency under 5 seconds&lt;br&gt;
Ability to handle spikes without losing events&lt;br&gt;
Failure rate under 1 percent with automatic retry&lt;/p&gt;

&lt;p&gt;AI: How will reliability be ensured?&lt;/p&gt;

&lt;p&gt;You:&lt;br&gt;
Use a queue to decouple processing&lt;br&gt;
Retry with exponential backoff&lt;br&gt;
Dead-letter queue for unprocessed events&lt;br&gt;
Idempotency to avoid duplicates&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Just like in the functional phase, this step produces a technical document that should be reviewed and approved.&lt;/p&gt;

&lt;p&gt;Don't let technical details rewrite the functional requirements — though in the real world, technology often ends up shaping business specs anyway.&lt;/p&gt;




&lt;h3&gt;
  
  
  Tasks
&lt;/h3&gt;

&lt;p&gt;By now we've turned an idea into two clear things: what to do and how to do it. Next comes execution.&lt;/p&gt;

&lt;p&gt;Translate the spec into clear, actionable tasks — exactly what the LLM or the developer should do, with no ambiguity. Start with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/sdd.plan&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The AI then generates something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AI: Here are the suggested tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TASK-001: Implement consumer for the &lt;code&gt;user.onboarding.completed&lt;/code&gt; event&lt;/li&gt;
&lt;li&gt;TASK-002: Create email-sending service with provider integration&lt;/li&gt;
&lt;li&gt;TASK-003: Implement idempotency logic to avoid duplicates&lt;/li&gt;
&lt;li&gt;TASK-004: Create email template with personalization&lt;/li&gt;
&lt;li&gt;TASK-005: Wire the full flow from event to send&lt;/li&gt;
&lt;li&gt;TASK-006: Add logs and metrics&lt;/li&gt;
&lt;li&gt;AUTO-TASK-001: Create unit tests&lt;/li&gt;
&lt;li&gt;AUTO-TASK-002: Create integration tests&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;These tasks can be saved in files such as &lt;code&gt;tasks.json&lt;/code&gt; or individually as &lt;code&gt;TASK-001.md&lt;/code&gt;, containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Status such as wip, done, or canceled&lt;/li&gt;
&lt;li&gt;Assignee&lt;/li&gt;
&lt;li&gt;Dependencies&lt;/li&gt;
&lt;li&gt;Possibility of parallelization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can refine with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/sdd.plan --refine&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And then approve with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/sdd.plan --approve&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This phase matters because it turns the spec into something executable. There should be no ambiguity here — only clear tasks.&lt;/p&gt;




&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;

&lt;p&gt;With everything approved, enter the implementation phase:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/sdd.build&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;During this phase, the AI executes the tasks and continuously validates the acceptance criteria from the functional spec, keeping the implementation aligned with the plan.&lt;/p&gt;

&lt;p&gt;Because the tasks are already well defined and decoupled, some tools let you parallelize executions that have no dependencies.&lt;/p&gt;

&lt;p&gt;One example is &lt;a href="https://code.claude.com/docs/en/agent-teams" rel="noopener noreferrer"&gt;Claude Agent Teams&lt;/a&gt;, which let you run multiple agents in parallel, each responsible for part of the system. Instead of a single AI doing everything sequentially, you can have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One agent implementing services&lt;/li&gt;
&lt;li&gt;Another writing tests&lt;/li&gt;
&lt;li&gt;Another handling integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All at the same time, respecting the dependencies defined in the tasks.&lt;/p&gt;

&lt;p&gt;This works especially well with SDD because the tasks are already well scoped — you don't need to manually coordinate who does what; the structure of the spec enables that split.&lt;/p&gt;

&lt;p&gt;Following the flow:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AI: Creating branch &lt;code&gt;feature/email-onboarding&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;AI: Starting task execution.&lt;/p&gt;

&lt;p&gt;AI: Running TASK-001: Event consumer&lt;/p&gt;

&lt;p&gt;AI: Consumer created successfully. Want to validate with tests?&lt;/p&gt;

&lt;p&gt;You: Yes.&lt;/p&gt;

&lt;p&gt;AI: Tests ran successfully.&lt;/p&gt;

&lt;p&gt;AI: Running TASK-002: Email-sending service&lt;/p&gt;

&lt;p&gt;AI: Provider integration complete. Retry implemented.&lt;/p&gt;

&lt;p&gt;AI: Tests passing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This process continues until all tasks are done.&lt;/p&gt;




&lt;h3&gt;
  
  
  Finish
&lt;/h3&gt;

&lt;p&gt;After implementation, wrap up the feature: make sure everything defined in the specs was delivered, validated, and is ready for production.&lt;/p&gt;

&lt;p&gt;You can start with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/sdd.finish&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The AI then runs final validations:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AI: Checking task status...&lt;/p&gt;

&lt;p&gt;AI: All tasks are complete.&lt;/p&gt;

&lt;p&gt;AI: Validating tests...&lt;/p&gt;

&lt;p&gt;AI: All tests are passing with adequate coverage.&lt;/p&gt;

&lt;p&gt;AI: Validating consistency between specs and implementation...&lt;/p&gt;

&lt;p&gt;AI: No divergences found.&lt;/p&gt;

&lt;p&gt;AI: Do you want to finish the feature?&lt;/p&gt;

&lt;p&gt;You: Yes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At that point, a few things usually happen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mark the feature as complete&lt;/li&gt;
&lt;li&gt;Ensure all specs are up to date&lt;/li&gt;
&lt;li&gt;Consolidate documentation&lt;/li&gt;
&lt;li&gt;Prepare for deploy or merge&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tips &amp;amp; tricks
&lt;/h2&gt;

&lt;p&gt;Don't limit yourself to ready-made setups — you can integrate this flow with day-to-day tools using MCPs like Jira or Linear to generate specs from tasks, GitHub to manage issues, branches, and comments, or Playwright to test a generated frontend.&lt;/p&gt;

&lt;p&gt;It's also common to have specialized agents for specific stages — code review, security validation, performance analysis. That makes the flow safer without adding manual complexity for the developer.&lt;/p&gt;

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

&lt;p&gt;You're no longer deciding what to do while writing code. That was already settled in the earlier phases. In a world where LLMs take an active part in development, this shrinks scope, improves delivery quality, and makes the process far more reproducible.&lt;/p&gt;

&lt;p&gt;In the end, it isn't about writing more code. It's about thinking better before you code (and letting the AI suffer a bit more in your place).&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>GO profiling using pprof</title>
      <dc:creator>Davi Orlandi</dc:creator>
      <pubDate>Mon, 15 Dec 2025 02:07:42 +0000</pubDate>
      <link>https://dev.to/dvorlandi/go-profilling-using-pprof-93i</link>
      <guid>https://dev.to/dvorlandi/go-profilling-using-pprof-93i</guid>
      <description>&lt;p&gt;Have you ever spent nights trying to understand why your application is using so many resources? Your CPU is on fire, memory keeps growing, latency goes up… and the code looks fine. Probably, you're &lt;strong&gt;guessing instead of verifying&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's how to verify memory leaks, unused resources, and poorly optimized code with Go's built-in profiling tool &lt;code&gt;pprof&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is pprof?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pprof&lt;/code&gt; is Go's built-in profiling tool for collecting and analyzing runtime data — CPU usage, memory allocations, goroutines, and blocking operations.&lt;/p&gt;

&lt;p&gt;In simple terms, pprof answers questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why is my application slow?&lt;/li&gt;
&lt;li&gt;Where is CPU time being spent?&lt;/li&gt;
&lt;li&gt;What is allocating so much memory?&lt;/li&gt;
&lt;li&gt;Are goroutines leaking?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Spoiler:&lt;/em&gt; pprof does not magically optimize your code. It just tells you &lt;strong&gt;where you messed up&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  How pprof works
&lt;/h2&gt;

&lt;p&gt;pprof works in two steps: &lt;strong&gt;collect&lt;/strong&gt; metrics from your application, then &lt;strong&gt;analyze&lt;/strong&gt; them with &lt;code&gt;go tool pprof&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At runtime, Go samples execution and records metrics. These samples are aggregated into profiles that you can explore with &lt;code&gt;pprof&lt;/code&gt; commands.&lt;/p&gt;

&lt;p&gt;You might think this is a lot like &lt;a href="https://en.wikipedia.org/wiki/Tracing_(software)" rel="noopener noreferrer"&gt;traces&lt;/a&gt;, but under the hood Go does &lt;a href="https://en.algorithmica.org/hpc/profiling/events" rel="noopener noreferrer"&gt;statistical profiling&lt;/a&gt;: your program is periodically interrupted and the runtime records what was running at that exact moment. Over time these snapshots form an accurate picture of resource use.&lt;/p&gt;

&lt;p&gt;A simple way to enable profiling in your app is using an HTTP server:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyy2cqwh6c7mg5bgppfwt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyy2cqwh6c7mg5bgppfwt.png" alt="main.go" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This exposes profiling endpoints under &lt;code&gt;/debug/pprof/&lt;/code&gt;, such as &lt;code&gt;/debug/pprof/profile&lt;/code&gt; (CPU), &lt;code&gt;/debug/pprof/heap&lt;/code&gt; (Memory), etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  Collecting profiles
&lt;/h3&gt;

&lt;p&gt;When you hit a pprof endpoint like &lt;code&gt;/debug/pprof/profile&lt;/code&gt;, what you get back is not a report — it's a binary blob. If you try to open it in the browser, you'll download a file and see what looks like complete garbage.&lt;/p&gt;

&lt;p&gt;That's expected: this endpoint returns a serialized pprof profile (a protobuf, usually gzip-compressed) with raw sampled data from the Go runtime — stack traces, counters, timestamps. Not charts. Not tables. Not answers. This binary mess is not meant for humans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And that's where &lt;code&gt;pprof&lt;/code&gt; comes in.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pprof&lt;/code&gt; takes this raw, unreadable data and decodes, aggregates, and correlates it into human-readable output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Analyzing profiles
&lt;/h3&gt;

&lt;p&gt;First, install the &lt;code&gt;pprof&lt;/code&gt; CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/google/pprof@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see something like that:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcckye3gqz8zqse41kmv3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcckye3gqz8zqse41kmv3.gif" alt="pprof install" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the web server running, collect a CPU profile (10 seconds):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go tool pprof http://localhost:8080/debug/pprof/profile?seconds&lt;span class="o"&gt;=&lt;/span&gt;10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything worked and you waited 10 seconds, you get an interactive shell. This is where the raw profile data becomes actionable. Once inside pprof, you can run commands like &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;, &lt;code&gt;web&lt;/code&gt;, etc. (check the options &lt;a href="https://github.com/google/pprof/blob/main/doc/README.md" rel="noopener noreferrer"&gt;here&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  The Big Bad Boy
&lt;/h2&gt;

&lt;p&gt;Imagine we have an endpoint using a lot of CPU — the "Big Bad Boy." For study purposes, I'll use a &lt;code&gt;/work&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqpexowixfuybenay9s5c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqpexowixfuybenay9s5c.png" alt="main.go" width="800" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the app running, hit &lt;code&gt;/work&lt;/code&gt; from another terminal to trigger some CPU load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8080/work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can repeat it a few times or use a load tool like &lt;a href="https://github.com/rakyll/hey" rel="noopener noreferrer"&gt;hey&lt;/a&gt; or &lt;a href="https://httpd.apache.org/docs/trunk/programs/ab.html" rel="noopener noreferrer"&gt;ab&lt;/a&gt; to generate more pressure.&lt;/p&gt;

&lt;p&gt;Now collect a 10-second CPU profile from the running server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go tool pprof http://localhost:6060/debug/pprof/profile?seconds&lt;span class="o"&gt;=&lt;/span&gt;10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the pprof shell, show the top functions consuming CPU with &lt;code&gt;top&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7d4s8zo8s5c4syvfvcfs.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7d4s8zo8s5c4syvfvcfs.gif" alt="top" width="800" height="833"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You'll see an output like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;pprof&lt;span class="o"&gt;)&lt;/span&gt; top 
Showing nodes accounting &lt;span class="k"&gt;for &lt;/span&gt;7470ms, 92.45% of 8080ms total
Dropped 2 nodes &lt;span class="o"&gt;(&lt;/span&gt;cum &amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; 40.40ms&lt;span class="o"&gt;)&lt;/span&gt;
Showing top 10 nodes out of 35
      flat  flat%   &lt;span class="nb"&gt;sum&lt;/span&gt;%        cum   cum%
    2030ms 25.12% 25.12%     2030ms 25.12%  math.archExp
    1570ms 19.43% 44.55%     1570ms 19.43%  math.IsInf &lt;span class="o"&gt;(&lt;/span&gt;inline&lt;span class="o"&gt;)&lt;/span&gt;
    1500ms 18.56% 63.12%     2830ms 35.02%  math.log
     560ms  6.93% 70.05%     5800ms 71.78%  math.pow
     430ms  5.32% 75.37%     1050ms 13.00%  math.sin
     330ms  4.08% 79.46%      330ms  4.08%  runtime.pthread_cond_signal
     320ms  3.96% 83.42%     1560ms 19.31%  math.frexp
     290ms  3.59% 87.00%      290ms  3.59%  math.Float64frombits &lt;span class="o"&gt;(&lt;/span&gt;inline&lt;span class="o"&gt;)&lt;/span&gt;
     220ms  2.72% 89.73%      220ms  2.72%  math.IsNaN &lt;span class="o"&gt;(&lt;/span&gt;inline&lt;span class="o"&gt;)&lt;/span&gt;
     220ms  2.72% 92.45%      230ms  2.85%  math.normalize &lt;span class="o"&gt;(&lt;/span&gt;inline&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can see a lot of &lt;code&gt;math&lt;/code&gt; package operations, but we don't know where those operations are used. Run &lt;code&gt;top&lt;/code&gt; again with the &lt;code&gt;-cum&lt;/code&gt; (cumulative) flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;pprof&lt;span class="o"&gt;)&lt;/span&gt; top &lt;span class="nt"&gt;-cum&lt;/span&gt;
Showing nodes accounting &lt;span class="k"&gt;for &lt;/span&gt;2170ms, 26.86% of 8080ms total
Dropped 2 nodes &lt;span class="o"&gt;(&lt;/span&gt;cum &amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; 40.40ms&lt;span class="o"&gt;)&lt;/span&gt;
Showing top 10 nodes out of 35
      flat  flat%   &lt;span class="nb"&gt;sum&lt;/span&gt;%        cum   cum%
     110ms  1.36%  1.36%     7750ms 95.92%  main.heavyComputation
         0     0%  1.36%     7750ms 95.92%  main.main.func1
         0     0%  1.36%     7750ms 95.92%  net/http.&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;ServeMux&lt;span class="o"&gt;)&lt;/span&gt;.ServeHTTP
         0     0%  1.36%     7750ms 95.92%  net/http.&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;conn&lt;span class="o"&gt;)&lt;/span&gt;.serve
         0     0%  1.36%     7750ms 95.92%  net/http.HandlerFunc.ServeHTTP
         0     0%  1.36%     7750ms 95.92%  net/http.serverHandler.ServeHTTP
         0     0%  8.29%     2830ms 35.02%  math.Log &lt;span class="o"&gt;(&lt;/span&gt;inline&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spotted! &lt;code&gt;main.heavyComputation&lt;/code&gt; is consuming 95.92% of the total CPU — that is our "Big Bad Boy."&lt;/p&gt;




&lt;h3&gt;
  
  
  You can do the same for memory, block, mutex, and goroutine profiles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Memory (heap) – see allocations and leaks:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go tool pprof http://localhost:6060/debug/pprof/heap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Goroutines – detect leaks or deadlocks:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go tool pprof http://localhost:6060/debug/pprof/goroutine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Block – see time spent waiting on channels, mutexes, etc.:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go tool pprof http://localhost:6060/debug/pprof/block
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Mutex – see contention hotspots:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go tool pprof http://localhost:6060/debug/pprof/mutex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of these can be analyzed the same way (&lt;code&gt;top&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;, &lt;code&gt;web&lt;/code&gt;, &lt;code&gt;svg&lt;/code&gt;). The &lt;code&gt;web&lt;/code&gt; and &lt;code&gt;svg&lt;/code&gt; commands produce &lt;strong&gt;graphical visualizations&lt;/strong&gt; (requires Graphviz), making it much easier to spot hotspots.&lt;/p&gt;




&lt;h3&gt;
  
  
  Visualization (Graphviz required)
&lt;/h3&gt;

&lt;p&gt;Graphical visualizations make it easier to spot CPU or memory hotspots quickly. Install &lt;a href="https://graphviz.org/" rel="noopener noreferrer"&gt;Graphviz&lt;/a&gt; first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;web&lt;/code&gt;: opens a call graph in your browser&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;svg&lt;/code&gt;: outputs an SVG call graph&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pdf&lt;/code&gt;: outputs a PDF graph&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Don't spray and pray
&lt;/h3&gt;

&lt;p&gt;pprof has multiple profile types because performance problems love to hide in different places. Each type answers a different question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU – where time is spent&lt;/li&gt;
&lt;li&gt;Heap – memory usage and leaks&lt;/li&gt;
&lt;li&gt;Goroutines – leaks or deadlocks&lt;/li&gt;
&lt;li&gt;Block – wait times&lt;/li&gt;
&lt;li&gt;Mutex – contention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Always choose the profile that matches the problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  ADDITIONAL: Profiling in Kubernetes
&lt;/h3&gt;

&lt;p&gt;If your app runs in a Kubernetes cluster, port-forward the pprof port from your pod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl port-forward deploy/my-app 6060:6060
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Access pprof locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go tool pprof http://localhost:6060/debug/pprof/profile?seconds&lt;span class="o"&gt;=&lt;/span&gt;10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Analyze interactively as usual (&lt;code&gt;top&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Useful Links &amp;amp; References
&lt;/h3&gt;

&lt;p&gt;Go pprof docs: &lt;a href="https://pkg.go.dev/net/http/pprof" rel="noopener noreferrer"&gt;https://pkg.go.dev/net/http/pprof&lt;/a&gt;&lt;br&gt;
Runtime profiling: &lt;a href="https://pkg.go.dev/runtime/pprof" rel="noopener noreferrer"&gt;https://pkg.go.dev/runtime/pprof&lt;/a&gt;&lt;br&gt;
go tool pprof guide: &lt;a href="https://go.dev/doc/diagnostics#profiling" rel="noopener noreferrer"&gt;https://go.dev/doc/diagnostics#profiling&lt;/a&gt;&lt;br&gt;
Official Go blog post about pprof: &lt;a href="https://go.dev/blog/pprof" rel="noopener noreferrer"&gt;https://go.dev/blog/pprof&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  So, Thanks!
&lt;/h2&gt;

&lt;p&gt;If this article saved you even one late night of guessing and blaming the garbage collector, it already did its job 😄. Thanks for reading!&lt;/p&gt;

&lt;h2&gt;
  
  
  See you soon ;)
&lt;/h2&gt;

</description>
      <category>go</category>
      <category>webdev</category>
      <category>distributedsystems</category>
      <category>programming</category>
    </item>
    <item>
      <title>Getting Started With Linux</title>
      <dc:creator>Davi Orlandi</dc:creator>
      <pubDate>Sun, 14 Dec 2025 22:36:44 +0000</pubDate>
      <link>https://dev.to/dvorlandi/getting-started-with-linux-4n7f</link>
      <guid>https://dev.to/dvorlandi/getting-started-with-linux-4n7f</guid>
      <description>&lt;p&gt;&lt;em&gt;This article is a study-oriented, introductory overview of Linux. It covers basic concepts and simplifies some topics to help beginners build an initial mental model — a starting point, not an exhaustive reference.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Base needed knowledge:
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GNU:&lt;/strong&gt; GNU is a predecessor of Linux and a free open-source Unix-like operating system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kernel:&lt;/strong&gt; The kernel is the core of an operating system. It acts as a bridge between software and hardware, managing system resources (CPU, memory, peripheral devices). Although people use "Linux" to describe the whole operating system, Linux itself refers only to the kernel.&lt;/p&gt;




&lt;h3&gt;
  
  
  What is Linux?
&lt;/h3&gt;

&lt;p&gt;Linux is an open-source operating system kernel originally created by Linus Torvalds. In practice, when people say "Linux," they usually mean a complete operating system: the Linux kernel combined with tools, libraries, and other software from the GNU project.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Linux system is divided into three layers:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Hardware&lt;/strong&gt;: The physical resources of your machine — CPU, memory, peripheral devices, etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linux Kernel&lt;/strong&gt;: The core of the operating system. It manages the hardware and facilitates communication between software and hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User Space&lt;/strong&gt;: The environment where users interact with the system, using applications and command-line interfaces.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a Linux distribution?
&lt;/h3&gt;

&lt;p&gt;A distribution is a bundle of the Linux kernel with specific software, tools, system utilities, libraries, and applications. Essentially, a distro is a complete, ready-to-use operating system built around the Linux kernel.&lt;/p&gt;




&lt;h2&gt;
  
  
  Debian:
&lt;/h2&gt;

&lt;p&gt;Debian is an operating system composed entirely of free and open-source software, and one of the most respected projects in the community.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package Management
&lt;/h3&gt;

&lt;p&gt;Debian uses a powerful package management tool called &lt;code&gt;apt&lt;/code&gt; (Advanced Package Tool). The system maintains a massive repository of pre-compiled software packages.&lt;/p&gt;




&lt;h2&gt;
  
  
  Red Hat Enterprise Linux (RHEL)
&lt;/h2&gt;

&lt;p&gt;RHEL is a commercial Linux distribution developed by Red Hat, built to provide long-term stability, security, and professional support.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package Management
&lt;/h3&gt;

&lt;p&gt;RHEL uses the RPM (Red Hat Package Manager) format for its software packages. For managing those packages, it provides package managers like YUM (Yellowdog Updater, Modifier) and its successor, DNF (Dandified YUM).&lt;/p&gt;




&lt;h2&gt;
  
  
  Ubuntu
&lt;/h2&gt;

&lt;p&gt;Ubuntu is probably the most famous Linux distribution — an excellent entry point for anyone looking to get started with Linux.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package Management
&lt;/h3&gt;

&lt;p&gt;As a Debian-based operating system, Ubuntu uses the core Debian package management system. That means it uses the &lt;code&gt;apt&lt;/code&gt; (Advanced Package Tool) command-line utility to handle software installation, updates, and removal, giving users access to a vast repository of free and open-source software.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fedora
&lt;/h2&gt;

&lt;p&gt;Fedora is roughly Ubuntu's counterpart from the Red Hat side — built on Red Hat's foundation instead of Debian.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package Management
&lt;/h3&gt;

&lt;p&gt;Fedora uses the RPM package format and manages software with the DNF (Dandified YUM) package manager. DNF is a command-line tool for installing, updating, and removing software packages.&lt;/p&gt;




&lt;h2&gt;
  
  
  The shell
&lt;/h2&gt;

&lt;p&gt;The shell is a program that accepts your typed commands and passes them to the operating system. If you've used a GUI (Graphical User Interface), you might have encountered applications like "Terminal" or "Console" — those are simply programs that open a shell session for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  BASH (Bourne Again Shell)
&lt;/h2&gt;

&lt;p&gt;Bash is the default shell for most Linux distros. While other shells like &lt;code&gt;ksh&lt;/code&gt;, &lt;code&gt;zsh&lt;/code&gt;, and &lt;code&gt;tcsh&lt;/code&gt; exist, mastering Bash provides a solid foundation for working with any Linux system.&lt;/p&gt;

&lt;p&gt;When you open a terminal, you'll see the shell prompt. Its appearance can vary between distros, but it typically follows this format: &lt;code&gt;username@hostname:current_directory$&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Yeah, I used a MacOS Terminal screenshot — don't care, it's not relevant.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fze0ry3m720zrst23dyul.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fze0ry3m720zrst23dyul.png" alt="Terminal" width="799" height="554"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;$&lt;/code&gt; symbol at the end indicates that the shell is ready to accept commands. You do not type this symbol when entering commands — it is purely informational!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How the hell does this work?&lt;/strong&gt;&lt;br&gt;
Just paste this command &lt;code&gt;echo "I love Linux"&lt;/code&gt; and you'll see "I love Linux" as the output in the console.&lt;/p&gt;


&lt;h2&gt;
  
  
  Filesystem basics
&lt;/h2&gt;

&lt;p&gt;Linux uses a single root filesystem, meaning everything starts from &lt;code&gt;/&lt;/code&gt; (root). There are no drive letters like &lt;code&gt;C:&lt;/code&gt; or &lt;code&gt;D:&lt;/code&gt; — all storage devices are mounted into the same tree.&lt;/p&gt;
&lt;h3&gt;
  
  
  Common directories
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/&lt;/code&gt; — root of the filesystem&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/home&lt;/code&gt; — user home directories&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/etc&lt;/code&gt; — system configuration files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/var&lt;/code&gt; — variable data (logs, cache, spool)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/usr&lt;/code&gt; — user-installed software and libraries&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/bin&lt;/code&gt; and &lt;code&gt;/sbin&lt;/code&gt; — essential system binaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Linux, &lt;strong&gt;everything is treated as a file&lt;/strong&gt;, including devices, processes, and sockets.&lt;/p&gt;
&lt;h3&gt;
  
  
  Paths
&lt;/h3&gt;

&lt;p&gt;A path is the location of a file or directory inside the filesystem. It tells the system where something is in the directory tree.&lt;/p&gt;

&lt;p&gt;Absolute paths start from &lt;code&gt;/&lt;/code&gt;, like &lt;code&gt;/home/user/projects&lt;/code&gt;. Relative paths omit the leading slash, like &lt;code&gt;projects/my-app&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Useful navigation commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;   &lt;span class="c"&gt;# show current directory&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt;    &lt;span class="c"&gt;# list files&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;    &lt;span class="c"&gt;# change directory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Permissions &amp;amp; users
&lt;/h2&gt;

&lt;p&gt;Linux is a multi-user operating system, built with security in mind from the start. Every file and directory has an owner, a group, and a set of permissions.&lt;/p&gt;

&lt;p&gt;You can see permissions on your computer with &lt;code&gt;ls -l&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgzesqx4zls5b3kyh6wpv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgzesqx4zls5b3kyh6wpv.png" alt="Terminal" width="800" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Root is the superuser, with full access. Normal users have limited permissions. You can run commands as root by prefixing them with &lt;code&gt;sudo&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Permissions are a core concept in Linux and one of the main reasons servers are secure by default.&lt;/p&gt;




&lt;h2&gt;
  
  
  Networking basics
&lt;/h2&gt;

&lt;p&gt;Linux provides built-in tools to inspect, debug, and interact with networks. Networking is a fundamental skill, especially for servers, containers, and cloud environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Network concepts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IP address&lt;/strong&gt;: identifies a machine on a network&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ports&lt;/strong&gt;: identify services running on a machine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protocols&lt;/strong&gt;: rules for communication (HTTP, TCP, UDP)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Common networking commands:
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ping "url.com"&lt;/code&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb2km21hge9lbfrtxbddr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb2km21hge9lbfrtxbddr.png" alt="Terminal" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;curl "url.com"&lt;/code&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3i1hkhdg4k3cpkq3y7ms.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3i1hkhdg4k3cpkq3y7ms.png" alt="Terminal" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wget "url.com/file.zip"&lt;/code&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn44hgh2o8qg54c5ce6kx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn44hgh2o8qg54c5ce6kx.png" alt="Terminal" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ip a&lt;/code&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd5r9lgg73gwrb58ghjha.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd5r9lgg73gwrb58ghjha.png" alt="Terminal" width="799" height="473"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Useful links &amp;amp; references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GNU&lt;/strong&gt; project: &lt;a href="https://www.gnu.org" rel="noopener noreferrer"&gt;https://www.gnu.org&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux Kernel&lt;/strong&gt; documentation: &lt;a href="https://www.kernel.org/doc/html/latest/" rel="noopener noreferrer"&gt;https://www.kernel.org/doc/html/latest/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filesystem Hierarchy Standard (FHS)&lt;/strong&gt;: &lt;a href="https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html" rel="noopener noreferrer"&gt;https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debian&lt;/strong&gt; official docs: &lt;a href="https://www.debian.org/doc/" rel="noopener noreferrer"&gt;https://www.debian.org/doc/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APT&lt;/strong&gt; documentation: &lt;a href="https://wiki.debian.org/Apt" rel="noopener noreferrer"&gt;https://wiki.debian.org/Apt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Red Hat Enterprise Linux&lt;/strong&gt; docs: &lt;a href="https://access.redhat.com/documentation" rel="noopener noreferrer"&gt;https://access.redhat.com/documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNF&lt;/strong&gt; documentation: &lt;a href="https://dnf.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;https://dnf.readthedocs.io/en/latest/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ubuntu&lt;/strong&gt; documentation: &lt;a href="https://help.ubuntu.com/" rel="noopener noreferrer"&gt;https://help.ubuntu.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fedora&lt;/strong&gt; documentation: &lt;a href="https://docs.fedoraproject.org/" rel="noopener noreferrer"&gt;https://docs.fedoraproject.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bash&lt;/strong&gt; manual: &lt;a href="https://www.gnu.org/software/bash/manual/" rel="noopener noreferrer"&gt;https://www.gnu.org/software/bash/manual/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux permissions&lt;/strong&gt; reference: &lt;a href="https://www.redhat.com/sysadmin/linux-file-permissions" rel="noopener noreferrer"&gt;https://www.redhat.com/sysadmin/linux-file-permissions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux networking&lt;/strong&gt; overview: &lt;a href="https://www.kernel.org/doc/html/latest/networking/index.html" rel="noopener noreferrer"&gt;https://www.kernel.org/doc/html/latest/networking/index.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Thanks!
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. I hope it clarified the core concepts behind Linux and gave you a starter foundation to keep exploring.&lt;/p&gt;

&lt;h2&gt;
  
  
  See you soon ;)
&lt;/h2&gt;

</description>
      <category>linux</category>
      <category>devops</category>
      <category>webdev</category>
      <category>distributedsystems</category>
    </item>
  </channel>
</rss>
