<?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: Pedro Rogério</title>
    <description>The latest articles on DEV Community by Pedro Rogério (@pinceladasdaweb).</description>
    <link>https://dev.to/pinceladasdaweb</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%2F430452%2F77516d62-cf12-421e-9309-b14b33c43000.jpg</url>
      <title>DEV Community: Pedro Rogério</title>
      <link>https://dev.to/pinceladasdaweb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pinceladasdaweb"/>
    <language>en</language>
    <item>
      <title>Node.js has plenty of circuit breakers. So why did I build another one?</title>
      <dc:creator>Pedro Rogério</dc:creator>
      <pubDate>Mon, 27 Jul 2026 12:40:00 +0000</pubDate>
      <link>https://dev.to/pinceladasdaweb/nodejs-has-plenty-of-circuit-breakers-so-why-did-i-build-another-one-3k66</link>
      <guid>https://dev.to/pinceladasdaweb/nodejs-has-plenty-of-circuit-breakers-so-why-did-i-build-another-one-3k66</guid>
      <description>&lt;p&gt;Every service I've worked on eventually grows the same scar tissue: a retry loop copy-pasted into six files, a circuit breaker bolted onto the payment client after an outage, a timeout wrapper someone wrote at 3 a.m. Each one slightly different. None of them talking to each other. And when things go wrong, nobody can answer the only question that matters during an incident: &lt;strong&gt;what is the resilience layer actually doing right now?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Java solved this years ago with &lt;a href="https://resilience4j.readme.io/" rel="noopener noreferrer"&gt;resilience4j&lt;/a&gt;. .NET has &lt;a href="https://github.com/App-vNext/Polly" rel="noopener noreferrer"&gt;Polly&lt;/a&gt;. Node.js... has pieces.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;I evaluated what the ecosystem offers before writing a single line:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/nodeshift/opossum" rel="noopener noreferrer"&gt;opossum&lt;/a&gt;&lt;/strong&gt; is the best-known circuit breaker, mature and well maintained. But it's &lt;em&gt;only&lt;/em&gt; a circuit breaker — retry is rudimentary, there's no bulkhead, no composition. Metrics need a plugin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/connor4312/cockatiel" rel="noopener noreferrer"&gt;cockatiel&lt;/a&gt;&lt;/strong&gt; is the closest thing to Polly: retry, breaker, timeout, bulkhead, composition. I genuinely like its design. But observability is where it stops — no native metrics, no pipeline-wide correlation — and maintenance has slowed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Sindre micro-libs&lt;/strong&gt; (&lt;code&gt;p-retry&lt;/code&gt;, &lt;code&gt;p-timeout&lt;/code&gt;, &lt;code&gt;p-limit&lt;/code&gt;) are excellent at exactly one thing each. But resilience is a &lt;em&gt;system&lt;/em&gt;: a retry that doesn't know the circuit is open will happily sleep through backoff to hammer a dead dependency. Isolated pieces can't coordinate.&lt;/p&gt;

&lt;p&gt;And there was one thing &lt;strong&gt;nobody&lt;/strong&gt; documented properly, which became the reason I finally started typing:&lt;/p&gt;

&lt;h2&gt;
  
  
  Ordering is the whole game
&lt;/h2&gt;

&lt;p&gt;Take four policies: retry, circuit breaker, timeout, fallback. The same four, nested in two different orders, produce two very different systems:&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="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;circuitBreaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// A&lt;/span&gt;
&lt;span class="nf"&gt;circuitBreaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// B&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;strong&gt;A&lt;/strong&gt;, every attempt flows through the breaker, so the breaker sees the dependency's &lt;em&gt;true&lt;/em&gt; failure rate — and when the circuit opens mid-retry, the retry finds out immediately.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;B&lt;/strong&gt;, the breaker sees one outcome per retry &lt;em&gt;cycle&lt;/em&gt;: three real failures against the dependency count as a single failure. The circuit opens far later than the dependency's actual state justifies, and the retry keeps sleeping through backoff against a broker that's clearly down.&lt;/p&gt;

&lt;p&gt;Most libraries let you build either one without ever telling you there's a difference. That's not an API problem — it's a documentation-of-consequences problem. I wanted a library where &lt;strong&gt;composition and its consequences are the headline feature&lt;/strong&gt;, not an afterthought.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/pinceladasdaweb/breakwater" rel="noopener noreferrer"&gt;breakwater&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;breakwater
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resilience&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;exponential&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;breakwater&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resilience&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;attempts&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="na"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;exponential&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;bulkhead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;circuitBreaker&lt;/span&gt;&lt;span class="p"&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="s1"&gt;payments-api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fallback&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;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;queued&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;charge&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;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/charge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;resilience()&lt;/code&gt; is the batteries-included path with a fixed, documented order:&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="nf"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;bulkhead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;circuitBreaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;fn&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;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each position is deliberate. The local guards (rate limit, bulkhead) sit &lt;em&gt;outside&lt;/em&gt; the breaker, because your own saturation must never open a circuit that describes the &lt;em&gt;dependency's&lt;/em&gt; health. The timeout sits innermost so every attempt gets its own budget — and a hung call becomes a countable failure instead of an invisible one.&lt;/p&gt;

&lt;p&gt;Need a different order? &lt;code&gt;compose()&lt;/code&gt; reads exactly like the nested calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitBreaker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;breakwater&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;attempts&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="c1"&gt;// outermost&lt;/span&gt;
  &lt;span class="nf"&gt;circuitBreaker&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="s1"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="c1"&gt;// innermost&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And because the result of &lt;code&gt;compose()&lt;/code&gt; is itself a policy, compositions compose again. The &lt;a href="https://github.com/pinceladasdaweb/breakwater/blob/main/docs/composition.md" rel="noopener noreferrer"&gt;ordering guide&lt;/a&gt; walks through the classic configurations with sequence diagrams — it's the page I wish every resilience library had.&lt;/p&gt;

&lt;h2&gt;
  
  
  Details that only show up in production
&lt;/h2&gt;

&lt;p&gt;A few behaviors took far more design effort than any diagram suggests, because they only matter when things are already going wrong:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Policies coordinate through errors.&lt;/strong&gt; Every error carries a stable &lt;code&gt;code&lt;/code&gt; and a &lt;code&gt;retryable&lt;/code&gt; flag. &lt;code&gt;CircuitOpenError&lt;/code&gt; declares itself &lt;code&gt;retryable: false&lt;/code&gt; — so the default retry gives up instantly instead of backing off against an open circuit. Rate-limit and bulkhead rejections stay retryable, because saturation is transient. This is why the pieces compose correctly &lt;em&gt;out of the box&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cancellation is not failure.&lt;/strong&gt; One &lt;code&gt;AbortSignal&lt;/code&gt; — combining your external cancellation, timeouts, everything — reaches your function. And when the caller aborts, nothing counts it as a failure: retry doesn't retry it, the breaker doesn't tally it, fallback doesn't replace it. Your user closing a tab should never open a circuit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeouts don't lie.&lt;/strong&gt; A genuine domain error that lands after the deadline propagates as itself, never masked as a &lt;code&gt;TimeoutError&lt;/code&gt;. A call the caller cancelled is cancellation, not a timeout. Your error dashboards reflect what actually happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitoring never changes an outcome.&lt;/strong&gt; An event listener or metrics collector that throws is isolated and reported — a bug in your telemetry cannot turn a successful payment into an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability without plugins
&lt;/h2&gt;

&lt;p&gt;Every policy emits typed events, one &lt;code&gt;correlationId&lt;/code&gt; crosses the whole pipeline, and stateful policies expose &lt;code&gt;stats()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;breaker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// { state: 'open', failureRate: 0.85, lastError, openedAt, nextAttemptAt, ... }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CircuitOpenError&lt;/code&gt; carries that snapshot too — a proper &lt;code&gt;Retry-After&lt;/code&gt; header is three lines. For metrics pipelines, implement one &lt;code&gt;MetricsCollector&lt;/code&gt; interface and it wires everywhere; define your policies once in a named registry and every metric comes out labeled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;policies&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;breakwater&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nx"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payments-api&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;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;attempts&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="na"&gt;circuitBreaker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// anywhere else — same name, same instance, genuinely shared circuit state&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payments-api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/charge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A health endpoint over every policy in your app is a &lt;code&gt;policies.names().map(...)&lt;/code&gt; away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dogfooding, honestly
&lt;/h2&gt;

&lt;p&gt;I maintain a &lt;a href="https://github.com/pinceladasdaweb/rabbitmq" rel="noopener noreferrer"&gt;RabbitMQ client library&lt;/a&gt; that had its own hand-rolled circuit breaker and retry loop — the exact scar tissue this post opened with. Migrating it to breakwater deleted the bespoke resilience code, upgraded the publisher to the correct retry-outside-the-breaker ordering, and &lt;strong&gt;every one of its existing tests passed unchanged&lt;/strong&gt;, including destructive reconnection tests against a real broker. That migration shipped; it's running in production today.&lt;/p&gt;

&lt;p&gt;It also taught me things no unit test would: that consumers of a hand-rolled retry expect the raw last error (there's a documented unwrap pattern now), and that sync call sites need a recreate-the-policy idiom for breaker resets. Both learnings went straight into the docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is — and isn't (yet)
&lt;/h2&gt;

&lt;p&gt;breakwater today: retry (backoff strategies + jitter + total deadline), timeout (cooperative and aggressive), circuit breaker (sliding windows, bounded half-open probing, manual isolation), bulkhead, rate limiting (token bucket and exact sliding window), chained fallbacks, composition, typed events, metrics, named policies. Zero runtime dependencies, TypeScript-native, dual ESM/CJS, Node &amp;gt;= 22.&lt;/p&gt;

&lt;p&gt;Planned and designed for (the circuit breaker's state store is already pluggable): &lt;strong&gt;distributed circuit breaker state over Redis&lt;/strong&gt; — ten instances of your service agreeing that an endpoint is down, with a single instance elected to probe recovery. Plus ready-made Prometheus/OpenTelemetry collectors and a stale-while-open response cache.&lt;/p&gt;

&lt;p&gt;The API is heading to 1.0; until then, minor versions may adjust it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/pinceladasdaweb/breakwater" rel="noopener noreferrer"&gt;github.com/pinceladasdaweb/breakwater&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt;: &lt;a href="https://www.npmjs.com/package/breakwater" rel="noopener noreferrer"&gt;npmjs.com/package/breakwater&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start here&lt;/strong&gt;: the &lt;a href="https://github.com/pinceladasdaweb/breakwater/blob/main/docs/composition.md" rel="noopener noreferrer"&gt;composition &amp;amp; ordering guide&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've ever debugged a retry storm at 3 a.m., I'd genuinely love your feedback — issues and PRs welcome. And if breakwater saves you from writing one more bespoke circuit breaker, it did its job.&lt;/p&gt;

</description>
      <category>node</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My Redis library said the write succeeded. Redis was down.</title>
      <dc:creator>Pedro Rogério</dc:creator>
      <pubDate>Fri, 24 Jul 2026 12:59:40 +0000</pubDate>
      <link>https://dev.to/pinceladasdaweb/my-redis-library-said-the-write-succeeded-redis-was-down-3dmn</link>
      <guid>https://dev.to/pinceladasdaweb/my-redis-library-said-the-write-succeeded-redis-was-down-3dmn</guid>
      <description>&lt;p&gt;Last year I &lt;a href="https://dev.to/pinceladasdaweb/my-rabbitmq-library-looked-production-ready-it-couldnt-survive-a-single-reconnect-1lp"&gt;audited my RabbitMQ library&lt;/a&gt; and discovered it couldn't survive a single reconnect. I turned the whole ordeal into a playbook, and I promised myself I'd run it against the next library in my drawer.&lt;/p&gt;

&lt;p&gt;The next library was a Redis client I wrote two years ago. The README advertised connection pooling, pub/sub, distributed locking, "battle-tested error handling" and "production-ready resilience".&lt;/p&gt;

&lt;p&gt;Here's what the audit found: there was no pooling. There was no pub/sub. There was no locking. There were no tests. And the resilience — the one thing a Redis wrapper exists for — was worse than absent. It was &lt;em&gt;actively lying&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is the story of the second time I learned the same lesson, plus the parts that were new.&lt;/p&gt;

&lt;h2&gt;
  
  
  The write that succeeded by doing nothing
&lt;/h2&gt;

&lt;p&gt;Every command in the library went through a health check. If Redis was unhealthy, this happened:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isHealthy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Redis is not healthy. Skipping &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; operation.`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that again. &lt;code&gt;set&lt;/code&gt;, &lt;code&gt;incr&lt;/code&gt;, &lt;code&gt;lpush&lt;/code&gt; — writes — would &lt;strong&gt;resolve successfully having done nothing&lt;/strong&gt;. No throw, no rejection. Your checkout flow calls &lt;code&gt;set&lt;/code&gt;, gets a resolved promise, tells the user their cart is saved. The cart does not exist.&lt;/p&gt;

&lt;p&gt;The best part? The README documented this as a feature: &lt;em&gt;"Methods return null when Redis is not healthy."&lt;/em&gt; I had documented data loss and called it graceful degradation.&lt;/p&gt;

&lt;p&gt;And for reads it's worse in a subtler way: &lt;code&gt;get&lt;/code&gt; returning &lt;code&gt;null&lt;/code&gt; means "key doesn't exist" — or now, "Redis was down". Your cache layer can't tell a miss from an outage.&lt;/p&gt;

&lt;p&gt;The fix is a contract, not a patch:&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RedisClientError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`Redis is not connected. Cannot execute '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;command&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;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;REDIS_UNAVAILABLE&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;Structured errors with a &lt;code&gt;code&lt;/code&gt;. The caller branches on &lt;code&gt;err.code === 'REDIS_UNAVAILABLE'&lt;/code&gt;, never on message text. A write that can't happen must be loud. This was a breaking change — and it's exactly why I fixed it &lt;em&gt;before&lt;/em&gt; the library had public users. Defaults are free to change until the day someone depends on them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reconnection layer that fought the driver — and lost to itself
&lt;/h2&gt;

&lt;p&gt;My library sits on top of ioredis. ioredis already reconnects. It has &lt;code&gt;retryStrategy&lt;/code&gt;, exponential backoff, an offline queue, resubscription. It is &lt;em&gt;good&lt;/em&gt; at this.&lt;/p&gt;

&lt;p&gt;Two-years-ago me didn't trust it, so he built a second reconnection layer on top. Every &lt;code&gt;error&lt;/code&gt; or &lt;code&gt;close&lt;/code&gt; event scheduled a manual reconnect:&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="nf"&gt;scheduleReconnect &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reconnectAttempts&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attemptConnection&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reconnectInterval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;attemptConnection &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;redisConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createRedisClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// ← a NEW client&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spot it? Every attempt created a &lt;strong&gt;brand-new ioredis client without destroying the previous one&lt;/strong&gt;. The old client — with its own retryStrategy — kept reconnecting forever in the background. Every drop leaked one more client, each running its own infinite retry loop against your server. A one-minute Redis outage turned my "resilience layer" into a connection storm.&lt;/p&gt;

&lt;p&gt;It gets better. After the first attempt, the validation ping was skipped entirely:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initialConnectionAttempted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ping&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initialConnectionAttempted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// every reconnection after this point:&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isConnected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;// sure, why not&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every reconnection "succeeded" unconditionally. &lt;code&gt;isConnected = true&lt;/code&gt; while Redis was on fire. The reconnection had &lt;strong&gt;never worked and never been tested&lt;/strong&gt; — the exact disease from the RabbitMQ library, in a different body.&lt;/p&gt;

&lt;p&gt;And my favorite: &lt;code&gt;disconnect()&lt;/code&gt; called &lt;code&gt;quit()&lt;/code&gt;, which made ioredis emit &lt;code&gt;close&lt;/code&gt;… and the &lt;code&gt;close&lt;/code&gt; handler scheduled a reconnect. &lt;strong&gt;Graceful shutdown resurrected the connection.&lt;/strong&gt; The library refused to die. There's something poetic about a resilience layer whose only reliably working feature is refusing to let you leave.&lt;/p&gt;

&lt;p&gt;The fix was not code. It was &lt;em&gt;deletion&lt;/em&gt;. The entire manual layer went away — the driver is the single owner of reconnection, and my library only tracks state and emits events. Four critical bugs died in one commit that removed more lines than it added.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test that earns the word "resilient"
&lt;/h2&gt;

&lt;p&gt;From the RabbitMQ refactor I kept one ritual: no reliability claim without a test that kills the connection &lt;em&gt;for real&lt;/em&gt;, from the &lt;em&gt;server side&lt;/em&gt;. For Redis, that's &lt;code&gt;CLIENT KILL&lt;/code&gt;:&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sacred:before&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// works&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;killAllClientsFromTheServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;               &lt;span class="c1"&gt;// CLIENT KILL, server-side&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sacred:after&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// works again, same client&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus the two regression probes that document the old bugs forever: after recovery, the server must see &lt;strong&gt;exactly the same number of connections&lt;/strong&gt; as before (no leaked reconnection loops), and after &lt;code&gt;disconnect()&lt;/code&gt; the client must &lt;strong&gt;stay disconnected&lt;/strong&gt; (no resurrection).&lt;/p&gt;

&lt;p&gt;First run against the old code: the resurrection probe failed exactly as predicted, and the test runner never exited — leaked timers kept the process alive. The suite needed Ctrl-C. When your test suite can't die, your library can't either.&lt;/p&gt;

&lt;p&gt;After the surgery: everything green, the runner exits by itself, and recovery after a server-side kill went from ~2 seconds to ~110 milliseconds — because reacting to the driver's events beats waiting for your own arbitrary timer.&lt;/p&gt;

&lt;h2&gt;
  
  
  A parade of small lies
&lt;/h2&gt;

&lt;p&gt;The deep read found plenty more. A sample:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The scan that read other people's keys.&lt;/strong&gt; &lt;code&gt;getAllStream()&lt;/code&gt; used ioredis' &lt;code&gt;scanStream&lt;/code&gt; — which does &lt;em&gt;not&lt;/em&gt; apply your &lt;code&gt;keyPrefix&lt;/code&gt; to the &lt;code&gt;MATCH&lt;/code&gt; pattern. With a prefix configured, scanning for &lt;code&gt;user:*&lt;/code&gt; matched &lt;strong&gt;zero&lt;/strong&gt; of your own keys. And scanning &lt;code&gt;*&lt;/code&gt; swept the &lt;em&gt;entire database&lt;/em&gt;, including other applications' keys, which then got your prefix wrongly added on the follow-up &lt;code&gt;GET&lt;/code&gt; and came back as &lt;code&gt;null&lt;/code&gt;. The old code had mysterious logging for these "null values", fetching TTL and type for each one. That wasn't observability. That was a bug filing reports about itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;maxRetryAttempts: 0&lt;/code&gt; meant infinity.&lt;/strong&gt; The classic &lt;code&gt;options.x || default&lt;/code&gt; — zero is falsy, so "never retry" became "retry forever". Same for &lt;code&gt;block: 0&lt;/code&gt; in stream reads, which in Redis means "block forever" and was silently dropped. &lt;code&gt;??&lt;/code&gt; exists. Use it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;XGROUP DESTROY&lt;/code&gt; couldn't work.&lt;/strong&gt; The wrapper appended a default stream id to every subcommand, so the destroy documented in my own README sent a stray &lt;code&gt;$&lt;/code&gt; and died with a protocol error. The README showed example code that had never once executed successfully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every command paid a toll.&lt;/strong&gt; The health check ran a real &lt;code&gt;PING&lt;/code&gt; — serialized &lt;em&gt;before&lt;/em&gt; your command, up to once per five seconds. Fail-fast checks must be local and free (&lt;code&gt;client.status === 'ready'&lt;/code&gt;); the driver already owns the expensive part.&lt;/p&gt;

&lt;p&gt;None of these are exotic. They're what the &lt;em&gt;first test you ever write&lt;/em&gt; catches. There were no tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I had to earn the README
&lt;/h2&gt;

&lt;p&gt;Deleting fiction from the README was easy. But two of the fictional features were genuinely good ideas — so this time they got built for real, with the reliability ritual applied from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pub/sub&lt;/strong&gt; lives on a dedicated connection (a subscribed Redis connection can't run normal commands — the library manages that for you), and its sacred test kills the subscriber server-side and proves messages flow again after recovery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distributed locking&lt;/strong&gt; is &lt;code&gt;SET NX PX&lt;/code&gt; with a random token, and release/extend go through Lua scripts that check the token — a holder whose lock expired can never delete the new holder's lock. The README says, in bold, that this is a single-instance mutex and &lt;strong&gt;not Redlock&lt;/strong&gt;. Honesty is a feature.&lt;/p&gt;

&lt;p&gt;And they compose. The cache helper uses the lock internally for stampede protection:&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;report&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrSetJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;report:daily&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buildReport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lock&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten concurrent cache misses, one producer call — the test proves exactly that, with real concurrency against a real Redis.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reviews that found what I couldn't
&lt;/h2&gt;

&lt;p&gt;"Looking finished" is still the most dangerous state, so finished code got adversarial reviews — every finding had to be &lt;em&gt;reproduced live&lt;/em&gt; before I accepted it. Two highlights that survived the process:&lt;/p&gt;

&lt;p&gt;A producer returning &lt;code&gt;undefined&lt;/code&gt; in the cache helper &lt;strong&gt;poisoned the key&lt;/strong&gt;: &lt;code&gt;JSON.stringify(undefined)&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt;, which got stored as an empty string with a TTL — and every later read exploded with &lt;code&gt;SyntaxError&lt;/code&gt; until it expired. One absent-minded &lt;code&gt;return&lt;/code&gt; and the key is bricked for an hour.&lt;/p&gt;

&lt;p&gt;And the packaging check — install the actual tarball in a clean project — caught two defects no unit test could: my &lt;code&gt;prepare&lt;/code&gt; script made npm v12 nag every consumer with an &lt;code&gt;allow-scripts&lt;/code&gt; approval warning, and TypeScript consumers in CommonJS hit &lt;code&gt;TS1479&lt;/code&gt; because a &lt;code&gt;"type": "module"&lt;/code&gt; package needs &lt;em&gt;dual&lt;/em&gt; declaration files (&lt;code&gt;index.d.ts&lt;/code&gt; + &lt;code&gt;index.d.cts&lt;/code&gt;). Validate the artifact, not the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;50 tests: 24 unit, 26 integration against a real Redis — including server-side &lt;code&gt;CLIENT KILL&lt;/code&gt; recovery for both commands and pub/sub&lt;/li&gt;
&lt;li&gt;1 runtime dependency (ioredis). The old version had three: pino and pino-pretty shipped a dev log formatter to every production install. Bring your own logger; the fallback is 25 dependency-free lines&lt;/li&gt;
&lt;li&gt;Fail-fast structured errors, observable lifecycle events, working optimistic locking via dedicated connections&lt;/li&gt;
&lt;li&gt;CI on Node 22/24 with a real Redis service container, npm trusted publishing (OIDC) with provenance, zero lifecycle scripts in the package&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's on npm as &lt;a href="https://www.npmjs.com/package/@pinceladasdaweb/redis" rel="noopener noreferrer"&gt;&lt;code&gt;@pinceladasdaweb/redis&lt;/code&gt;&lt;/a&gt;, source at &lt;a href="https://github.com/pinceladasdaweb/redis" rel="noopener noreferrer"&gt;github.com/pinceladasdaweb/redis&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lessons, second edition
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Docs are a spec you've already published.&lt;/strong&gt; Either the code honors them or the docs change. My README described a different, better library — and the gap was invisible because nothing tested the claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read everything before fixing anything.&lt;/strong&gt; The four worst bugs looked independent and were one defect: a reconnection layer that shouldn't exist. Patch-by-patch would have polished each symptom and kept the disease.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't own what your driver owns.&lt;/strong&gt; The best resilience code I wrote this month was a deletion. If you're wrapping a client that already reconnects, your job is what happens &lt;em&gt;around&lt;/em&gt; reconnection — state, events, dedicated connections — not a competing loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A write that resolves while doing nothing is data loss with good manners.&lt;/strong&gt; Fail fast, fail loud, fail with a &lt;code&gt;code&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kill it from the server side.&lt;/strong&gt; &lt;code&gt;CLIENT KILL&lt;/code&gt; is one command. If your library claims resilience and your test suite has never dropped a connection for real, you don't have a resilience feature — you have a resilience rumor.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same ending as last time, because it's still true: this library now survives everything I could think of throwing at it, and it has zero production mileage under other people's workloads. If you run Redis with Node and feel like breaking something, I'd genuinely love the bug reports.&lt;/p&gt;

&lt;p&gt;Turns out "looking finished" is still the bug. At least this time I knew where to look.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>node</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My RabbitMQ library looked production-ready. It couldn't survive a single reconnect.</title>
      <dc:creator>Pedro Rogério</dc:creator>
      <pubDate>Sun, 19 Jul 2026 17:13:46 +0000</pubDate>
      <link>https://dev.to/pinceladasdaweb/my-rabbitmq-library-looked-production-ready-it-couldnt-survive-a-single-reconnect-1lp</link>
      <guid>https://dev.to/pinceladasdaweb/my-rabbitmq-library-looked-production-ready-it-couldnt-survive-a-single-reconnect-1lp</guid>
      <description>&lt;p&gt;For a couple of years I had a RabbitMQ client library sitting in a private repo. On paper it was impressive: automatic reconnection, channel pooling, a circuit breaker, four rate-limiting strategies, gzip compression, dead letter queues, worker-thread consumers, delayed messages. Over 1,200 lines in the main class alone.&lt;/p&gt;

&lt;p&gt;I never quite trusted it enough to publish. Recently I decided to find out whether that instinct was right.&lt;/p&gt;

&lt;p&gt;It was. &lt;strong&gt;The single most important feature of a message-broker client — surviving a connection drop — had never worked.&lt;/strong&gt; Not "worked poorly". Never worked. And nothing in the repo could have told me, because there wasn't a single test.&lt;/p&gt;

&lt;p&gt;This is the story of the audit, the rebuild, and the release of &lt;a href="https://www.npmjs.com/package/@pinceladasdaweb/rabbitmq" rel="noopener noreferrer"&gt;&lt;code&gt;@pinceladasdaweb/rabbitmq&lt;/code&gt;&lt;/a&gt;. It's also a collection of lessons I wish I'd internalized years earlier, because none of these bugs were exotic. They were all sitting in plain sight, waiting for a failure path to be exercised for the first time in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  The audit: what "looks done" actually hides
&lt;/h2&gt;

&lt;p&gt;The library had the shape of a finished product. Rich API, detailed README, twenty example scripts. Here's a sample of what a deep read of the code actually found.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The reconnection that reconnected to nothing.&lt;/strong&gt; The connection layer dutifully detected drops, retried with exponential backoff, and re-established the TCP connection. Then... nothing. The channel pool — the thing every publish and consume goes through — was only ever created inside &lt;code&gt;connect()&lt;/code&gt;, and the reconnection path never called it. After any network blip, every operation would throw &lt;code&gt;Not connected&lt;/code&gt; forever, while the connection state proudly reported &lt;code&gt;connected&lt;/code&gt;. The recovery code &lt;em&gt;existed&lt;/em&gt;; it recreated consumers using channels captured in closures from before the drop — channels that were dead. It had clearly never been executed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rate limiter that crashed on its own feature.&lt;/strong&gt; The block-a-key feature stored blocked keys in a &lt;code&gt;Set&lt;/code&gt;... and called &lt;code&gt;this.blocked.set(key, Date.now())&lt;/code&gt; on it. &lt;code&gt;Set&lt;/code&gt; has no &lt;code&gt;.set()&lt;/code&gt;. Calling &lt;code&gt;blockKey()&lt;/code&gt; threw a &lt;code&gt;TypeError&lt;/code&gt;, every time, since the day it was written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The circuit breaker state that didn't exist.&lt;/strong&gt; The public API exposed &lt;code&gt;getCircuitBreakerState()&lt;/code&gt;, which called &lt;code&gt;this.#circuitBreaker.getState()&lt;/code&gt; — a method the circuit breaker class simply did not have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dead-letter helper with the wrong signature.&lt;/strong&gt; &lt;code&gt;moveToDeadLetter()&lt;/code&gt; called the publish method with four arguments in the wrong order, so the &lt;em&gt;name of the queue&lt;/em&gt; was published as the message body, to the wrong exchange.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The library that killed your process.&lt;/strong&gt; The constructor registered &lt;code&gt;SIGINT&lt;/code&gt;, &lt;code&gt;SIGTERM&lt;/code&gt;, &lt;code&gt;uncaughtException&lt;/code&gt; and &lt;code&gt;unhandledRejection&lt;/code&gt; handlers — and called &lt;code&gt;process.exit()&lt;/code&gt; in them. A &lt;em&gt;library&lt;/em&gt;. Any unhandled rejection anywhere in your application would cause my dependency to shut your process down.&lt;/p&gt;

&lt;p&gt;None of these are subtle bugs. They're the kind of thing the &lt;em&gt;first test you ever write&lt;/em&gt; catches. Which brings me to the first lesson.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lesson 1: features are cheap; invariants are expensive.&lt;/strong&gt; An API surface can look complete while every failure path is fiction. The happy path is maybe 20% of an infrastructure library — the rest is what happens during the bad minutes, and that's exactly the part that "it runs on my machine" never exercises.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  If you claim resilience, your CI has to actually break things
&lt;/h2&gt;

&lt;p&gt;The rebuild started with the reliability core: reconnection has to restore the channel pool, re-assert topology, and recreate every consumer on fresh channels. But I'd been burned by "code that looks like it recovers", so the real work was making the claim &lt;em&gt;falsifiable&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The integration suite now runs against a real RabbitMQ (Docker locally, a service container in GitHub Actions) and includes this test — the one I care most about in the entire repo:&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recovers channel pool and consumers after a forced connection drop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;reconnected&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;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;rabbitMQ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reconnected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="c1"&gt;// Ask the broker to close every AMQP connection, simulating a real outage.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;forceCloseAllConnections&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// DELETE /api/connections/* via the management API&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reconnected&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;countBefore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;received&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rabbitMQ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROUTING_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reconnect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;received&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;countBefore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rabbitMQ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClusterStatus&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;connectionState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;connected&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No mocks. The broker genuinely kills the connection; the library genuinely has to come back: pool rebuilt, exchange re-asserted, consumer re-subscribed on a new channel, message flowing again. This runs on every pull request.&lt;/p&gt;

&lt;p&gt;Using the management HTTP API for the kill switch (instead of &lt;code&gt;docker restart&lt;/code&gt;) was a small trick that paid off: it works identically on a laptop and inside a CI service container, and it's fast.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lesson 2: a resilience feature without a test that injects the failure is a rumor.&lt;/strong&gt; The test that kills your broker connection is worth more than the thousand lines of recovery code it validates.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The adversarial review: 42 candidates, 10 confirmed bugs
&lt;/h2&gt;

&lt;p&gt;With the core rebuilt and a test harness in place, I ran a structured review of the source: multiple independent passes over the code, each hunting from a different angle — line-by-line, lifecycle invariants, cross-module contracts, plus efficiency and design-depth sweeps. Every candidate finding then got adversarially verified against the code before I accepted it.&lt;/p&gt;

&lt;p&gt;Ten confirmed bugs came out. Three of my favorites, because each one represents a &lt;em&gt;category&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The option that silently discarded messages.&lt;/strong&gt;&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="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;retryOperation &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxRetries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&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="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&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;Spot it? Pass &lt;code&gt;{ maxRetries: 0 }&lt;/code&gt; — a perfectly reasonable "try once, don't retry" intent — and the loop body never runs. The operation is never attempted, &lt;code&gt;channel.publish&lt;/code&gt; is never called, and the promise &lt;strong&gt;resolves successfully&lt;/strong&gt;. The circuit breaker even records a success. Silent message loss, triggered by a documented option. The fix is one line (&lt;code&gt;Math.max(1, maxRetries)&lt;/code&gt;), plus a regression test that pins the semantics forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The poison message that ate a consumer.&lt;/strong&gt; A message with a corrupt gzip body would fail to decode, get nacked with &lt;code&gt;requeue: true&lt;/code&gt;, be redelivered instantly, fail again... forever. With &lt;code&gt;prefetch: 1&lt;/code&gt;, one bad message parked an entire queue while burning 100% CPU. Undecodable messages now go to the dead letter queue — failure handling should &lt;em&gt;drain&lt;/em&gt; problems, not recirculate them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The unroutable dead-letter that vanished.&lt;/strong&gt; Moving a message to a DLQ published it without the &lt;code&gt;mandatory&lt;/code&gt; flag. If the dead-letter routing had no binding — one topology mistake away — the broker would &lt;em&gt;confirm&lt;/em&gt; the publish and silently drop the message. The method resolved, logged success, and the message ceased to exist. It now publishes with &lt;code&gt;mandatory: true&lt;/code&gt;, listens for &lt;code&gt;basic.return&lt;/code&gt;, and rejects loudly when the DLQ isn't routable.&lt;/p&gt;

&lt;p&gt;There were seven more: a &lt;code&gt;disconnect()&lt;/code&gt;/&lt;code&gt;connect()&lt;/code&gt; cycle that permanently disabled auto-reconnection, consumer callback failures tripping the circuit breaker that gates &lt;em&gt;publishing&lt;/em&gt;, a fire-and-forget publish that could hang forever awaiting &lt;code&gt;'drain'&lt;/code&gt; on a dead channel, duplicate consumers spawned by racing recovery paths...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lesson 3: bugs cluster in the seams.&lt;/strong&gt; Almost every confirmed finding lived at an intersection — between two recovery mechanisms, between an option's edge value and a default, between "the callback succeeded" and "the ack was delivered". Reviews that only read functions in isolation don't find these. You have to trace the &lt;em&gt;interactions&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Your test suite also protects you from everyone else
&lt;/h2&gt;

&lt;p&gt;Two things happened during this project that had nothing to do with my code, and the test suite caught both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dependency major-bump.&lt;/strong&gt; Mid-rebuild, I upgraded &lt;code&gt;amqplib&lt;/code&gt; from 0.10 to 2.0 — a jump across two majors of the library this whole thing wraps. The entire integration suite (confirms, compression round-trips, forced reconnections, delayed messages) passed against the new version in one afternoon. Without the suite, that upgrade would have been an act of faith deferred indefinitely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The floating Docker tag.&lt;/strong&gt; My CI used &lt;code&gt;rabbitmq:4-management-alpine&lt;/code&gt;. One day that tag started resolving to RabbitMQ 4.3, which rejected the delayed-message plugin version I bundled (built for 4.0.x). CI went red before any user ever saw the problem. The fix taught me to &lt;strong&gt;pin brokers and plugins as a matched pair&lt;/strong&gt; (&lt;code&gt;rabbitmq:4.2-management-alpine&lt;/code&gt; + plugin &lt;code&gt;4.2.0&lt;/code&gt;) — a floating major tag in CI is a time bomb with a pleasant default.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lesson 4: automated tests are not just about your bugs.&lt;/strong&gt; They're your early-warning system for upstream majors, ecosystem drift, and infrastructure that changes underneath you.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Dependencies are promises
&lt;/h2&gt;

&lt;p&gt;At the start, the library shipped four runtime dependencies, including &lt;code&gt;pino&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;pino-pretty&lt;/code&gt; — a pretty-printer for development logs, in every production install.&lt;/p&gt;

&lt;p&gt;It now ships &lt;strong&gt;two&lt;/strong&gt;: &lt;code&gt;amqplib&lt;/code&gt; and a small cache. The logger question is the interesting one. The library used to create its own pino instance at import time — its own format, its own destination, disconnected from whatever the host application does. But an infrastructure library shouldn't own a logging pipeline; it should write to whatever you give it:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;pino&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pino&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rabbit&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;RabbitMQ&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost:5672&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;pino&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="s1"&gt;rabbitmq&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="c1"&gt;// any pino/winston/bunyan instance works&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you inject nothing, a 25-line dependency-free console logger provides timestamped, leveled output. Nobody pays for a logging stack they already have.&lt;/p&gt;

&lt;p&gt;The same thinking applied to process lifecycle: the library no longer touches your signal handlers. Graceful shutdown is one explicit call — &lt;code&gt;rabbit.enableGracefulShutdown()&lt;/code&gt; — and it's opt-in, because your process belongs to you, not to your dependencies.&lt;/p&gt;




&lt;h2&gt;
  
  
  A small release-engineering war story
&lt;/h2&gt;

&lt;p&gt;The publish pipeline (GitHub Actions → npm on every merge to &lt;code&gt;main&lt;/code&gt;) had its own baptism by fire. Two merges landed on &lt;code&gt;main&lt;/code&gt; in quick succession; two release runs raced each other. The first published &lt;code&gt;1.0.1&lt;/code&gt; to npm, then failed pushing the git tag back (the branch had moved). The second run recomputed "next version = 1.0.1" from the now-stale &lt;code&gt;package.json&lt;/code&gt; — and npm refused to publish over an existing version. Deadlock: every subsequent release run would fail the same way.&lt;/p&gt;

&lt;p&gt;Two structural fixes, both worth stealing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The registry is the source of truth.&lt;/strong&gt; The workflow now asks npm what the latest published version is (&lt;code&gt;npm view &amp;lt;pkg&amp;gt; version&lt;/code&gt;) and bumps from &lt;em&gt;that&lt;/em&gt;, never from &lt;code&gt;package.json&lt;/code&gt;. Whatever half-finished state a previous run left behind, the next run self-corrects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serialize your releases.&lt;/strong&gt; A &lt;code&gt;concurrency: { group: publish }&lt;/code&gt; block means rapid merges queue instead of racing.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lesson 5: release automation fails in the middle, not at the edges.&lt;/strong&gt; Design every step to be safe to re-run, and derive state from systems of record, not from files a previous failed run may or may not have updated.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Where it landed
&lt;/h2&gt;

&lt;p&gt;The library is now public: &lt;a href="https://www.npmjs.com/package/@pinceladasdaweb/rabbitmq" rel="noopener noreferrer"&gt;&lt;code&gt;@pinceladasdaweb/rabbitmq&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @pinceladasdaweb/rabbitmq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;RabbitMQ&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pinceladasdaweb/rabbitmq&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rabbit&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;RabbitMQ&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost:5672&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;exchange&lt;/span&gt;&lt;span class="p"&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="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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;direct&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rabbit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rabbit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order-processing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// auto-ack on success, nack → DLQ on throw&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rabbit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order.created&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="mi"&gt;42&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="c1"&gt;// publisher-confirmed&lt;/span&gt;

&lt;span class="nx"&gt;rabbit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reconnected&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="o"&gt;=&amp;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;survived an outage, consumers restored&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;What's in the box: automatic reconnection with full state recovery (pool, topology, consumers), publisher confirms on everything, per-key rate limiting (token bucket, leaky bucket, fixed and sliding windows), a circuit breaker that's isolated from consumer failures, transparent gzip compression, first-class dead-letter support (&lt;code&gt;createQueue&lt;/code&gt;, &lt;code&gt;moveToDeadLetter&lt;/code&gt;, &lt;code&gt;processDeadLetterQueue&lt;/code&gt;), delayed messages via the official plugin, worker-thread parallel consumers with automatic respawn, dependency-ordered sequential processing, consumer lifecycle management (&lt;code&gt;unsubscribe&lt;/code&gt;, &lt;code&gt;consumerCancelled&lt;/code&gt;/&lt;code&gt;consumerRecovered&lt;/code&gt;/&lt;code&gt;consumerLost&lt;/code&gt; events), and TypeScript declarations checked in strict mode on CI.&lt;/p&gt;

&lt;p&gt;Guarding all of it: &lt;strong&gt;87 automated tests&lt;/strong&gt; — 72 unit, 15 integration against a real broker including the forced-outage scenario — running on Node 22 and 24 on every pull request, plus 22 runnable examples that double as living documentation.&lt;/p&gt;

&lt;p&gt;And in the spirit of the whole post, the honest part: this library has excellent test coverage and zero production mileage under other people's workloads. Those are different kinds of confidence, and only one of them can be built alone. If you run RabbitMQ in Node and any of this resonates, I'd genuinely love an issue, a hard question, or a war story from your queue topology: &lt;a href="https://github.com/pinceladasdaweb/rabbitmq" rel="noopener noreferrer"&gt;github.com/pinceladasdaweb/rabbitmq&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The library I was too embarrassed to publish taught me more in this rebuild than it did in all the years it sat in a folder looking finished. Turns out "looking finished" was the bug.&lt;/p&gt;

</description>
      <category>node</category>
      <category>rabbitmq</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Node URL Shortener</title>
      <dc:creator>Pedro Rogério</dc:creator>
      <pubDate>Fri, 11 Jun 2021 19:49:08 +0000</pubDate>
      <link>https://dev.to/pinceladasdaweb/node-url-shortener-4234</link>
      <guid>https://dev.to/pinceladasdaweb/node-url-shortener-4234</guid>
      <description>&lt;p&gt;I recently ended up creating a URL shortener using NodeJS, Fastify, Postgres and Redis for study purposes. If you think it might be useful for you, if only for study purposes too, come take a look: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/pinceladasdaweb/node-url-shortener"&gt;https://github.com/pinceladasdaweb/node-url-shortener&lt;/a&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>postgres</category>
      <category>redis</category>
      <category>fastify</category>
    </item>
    <item>
      <title>Developing a RESTful API using Fastify</title>
      <dc:creator>Pedro Rogério</dc:creator>
      <pubDate>Mon, 08 Feb 2021 10:58:57 +0000</pubDate>
      <link>https://dev.to/pinceladasdaweb/developing-a-restful-api-using-fastify-4763</link>
      <guid>https://dev.to/pinceladasdaweb/developing-a-restful-api-using-fastify-4763</guid>
      <description>&lt;p&gt;You who are tired of seeing examples of REST API in Node using Express will be very interested in this boilerplate that I ended up developing using Traefik, Docker, Docker Compose, Fastify, JWT and Mongodb.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/pinceladasdaweb/docker-fastify-restful-api"&gt;https://github.com/pinceladasdaweb/docker-fastify-restful-api&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
