<?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: Luca Giordano</title>
    <description>The latest articles on DEV Community by Luca Giordano (@lsfera).</description>
    <link>https://dev.to/lsfera</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%2F1433871%2F60bf07e5-7f36-4066-882b-686f634b5d74.png</url>
      <title>DEV Community: Luca Giordano</title>
      <link>https://dev.to/lsfera</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lsfera"/>
    <language>en</language>
    <item>
      <title>Circuit Breaker: Unboxing — your message broker is already the control plane</title>
      <dc:creator>Luca Giordano</dc:creator>
      <pubDate>Thu, 01 Oct 2026 07:36:30 +0000</pubDate>
      <link>https://dev.to/lsfera/circuit-breaker-unboxing-your-message-broker-is-already-the-control-plane-3epn</link>
      <guid>https://dev.to/lsfera/circuit-breaker-unboxing-your-message-broker-is-already-the-control-plane-3epn</guid>
      <description>&lt;p&gt;A recent interview put me in front of this exact use case, and I gave an average answer that left a bad taste in my mouth. It kept nagging me, so I built it five times and measured each one.&lt;/p&gt;

&lt;p&gt;"Add a circuit breaker" usually means adding a library to every service that calls the flaky dependency. That works when the caller is one process. It breaks down when the caller is a &lt;strong&gt;fleet&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiblb0i71gf4ud1az170q.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiblb0i71gf4ud1az170q.gif" alt="Four frames of the same producer, work queue, consumers and third party, one per step: no breaker, a breaker in every process, a breaker coordinated by the broker, and a platform control plane" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;One producer, one durable RabbitMQ queue, and a fleet of consumers competing for its messages. Each message becomes one call to a third party whose load balancer, health checks and hosts are entirely theirs.&lt;/p&gt;

&lt;p&gt;The textbook breaker assumes one caller and one dependency. We have neither:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Many callers, one opinion needed.&lt;/strong&gt; A breaker per consumer means each forms its own view from its own sample.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Their load balancer, not ours.&lt;/strong&gt; We see one address, so we can only decide &lt;em&gt;when&lt;/em&gt; to stop calling them, not which host to avoid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whose failure is it, anyway?&lt;/strong&gt; A request shed by our own egress proxy can look identical to an upstream failure. With an upstream that was only 300 ms slower, every 5xx our callers saw came from our own proxy: 90,753 local errors in two minutes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A breaker with no control plane floods
&lt;/h2&gt;

&lt;p&gt;Step 1 has no breaker: every message spends its three delivery attempts against a dead upstream, and about &lt;strong&gt;4,000 messages are dead-lettered in 20 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Step 2 puts a standard breaker (&lt;a href="https://github.com/connor4312/cockatiel" rel="noopener noreferrer"&gt;cockatiel&lt;/a&gt;) in every consumer. It does its one job: each replica stops after five failures. But nobody shares the verdict:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one 15-second outage opens the fleet's five breakers &lt;strong&gt;about 27 times&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;all five agree on the state only &lt;strong&gt;56% of the time&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;each half-open probe runs on its own clock;&lt;/li&gt;
&lt;li&gt;and &lt;strong&gt;1,577–2,246 messages are dead-lettered&lt;/strong&gt; from just 52–59 real failures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last number is the lesson. &lt;strong&gt;Most of the loss came from what "open" did, not from the outage.&lt;/strong&gt; Every call an open breaker turned away still spent one of the message's delivery attempts, so messages the third party never saw ended up in the dead-letter queue.&lt;/p&gt;

&lt;p&gt;What's missing is a control plane: something the fleet shares that says whether to call and who probes.&lt;/p&gt;

&lt;h2&gt;
  
  
  RabbitMQ already is one
&lt;/h2&gt;

&lt;p&gt;Steps 3 and 4 add no new infrastructure. Everything the fleet needs to agree on is a broker feature:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The fleet needs&lt;/th&gt;
&lt;th&gt;RabbitMQ gives it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;one probe for the whole fleet&lt;/td&gt;
&lt;td&gt;a one-token queue (&lt;code&gt;x-max-length: 1&lt;/code&gt;, &lt;code&gt;x-overflow: reject-publish&lt;/code&gt;) as the probe permit. With every replica open, peak probes in flight went from 6 to &lt;strong&gt;1&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;one replica for jobs only one should do (replaying dead letters)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;x-single-active-consumer&lt;/code&gt;: the broker elects one consumer and hands over when it disconnects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;turning a message away without spending its budget&lt;/td&gt;
&lt;td&gt;from &lt;strong&gt;RabbitMQ 4.3&lt;/strong&gt;, a requeuing &lt;code&gt;nack&lt;/code&gt; no longer counts toward &lt;code&gt;x-delivery-limit&lt;/code&gt;, while a &lt;code&gt;reject&lt;/code&gt; still does&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"open" held somewhere other than process memory&lt;/td&gt;
&lt;td&gt;step 4: a consumer switched off, and a token waiting in a delay chain built from per-queue TTLs and dead-lettering, with no plugin and no timer in the process&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Plus the thing a separate control plane would not give you: the work itself.&lt;/strong&gt; While the breaker is open, messages wait durably in the queue that already holds them, instead of being tried, refused and dead-lettered by every replica.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Dead-lettered in an outage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Step 2 · a breaker in every process&lt;/td&gt;
&lt;td&gt;1,577–2,246 in 15 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Step 3 · coordinated by the broker&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0&lt;/strong&gt; in 40 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Step 4 · held by the broker&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0&lt;/strong&gt;, where cockatiel lost 2,745&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The breaker got simpler, not bigger: by step 4 it keeps no state of its own.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The version matters.&lt;/strong&gt; On a broker older than 4.3 every requeue counts toward the delivery limit, and a message waiting out an open breaker gets dead-lettered untried.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When the verdict has to leave the fleet
&lt;/h2&gt;

&lt;p&gt;For most consumer fleets, step 3 or 4 is where to stop. Step 5 is for when other systems need to act on the verdict: a producer that stops accepting work, a status page, billing.&lt;/p&gt;

&lt;p&gt;There, Envoy enforces per replica (outlier detection, concurrency limits) and a leader-elected aggregator takes a quorum of what the replicas report. It publishes &lt;strong&gt;one verdict per API&lt;/strong&gt; as events with a gapless sequence. The fleet still coordinates through RabbitMQ: a topic exchange carries each verdict to a queue per consumer, and single-active-consumer queues elect the one consumer that probes, redrives or keeps a minimum of work going.&lt;/p&gt;

&lt;p&gt;Across ten chaos faults (killing consumers, the leading aggregator, the broker, a hanging upstream, a lease partition mid-outage) it lost &lt;strong&gt;0 messages&lt;/strong&gt; and dead-lettered &lt;strong&gt;0&lt;/strong&gt;. It costs about three times the code of step 3 and 19 containers, and it pays only when something outside the fleet consumes the verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A breaker is four decisions&lt;/strong&gt;: what counts as a failure, where the state lives, what "open" does, and who decides to try again. A library answers all four together; pulling them apart lets you answer each one better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For a fleet, "open" should mean stop consuming&lt;/strong&gt;, not reject the message. Your broker already knows how to hold work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at your broker before adding a coordination service.&lt;/strong&gt; Single-active-consumer, delivery limits, TTLs and dead-lettering already give you a probe permit, leader election and a durable timer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure what "open" costs&lt;/strong&gt;, not just whether the breaker trips.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The full article, with each step's design and measurements, is at &lt;strong&gt;&lt;a href="https://lsfera.github.io/circuit-breaker-unboxing/" rel="noopener noreferrer"&gt;lsfera.github.io/circuit-breaker-unboxing&lt;/a&gt;&lt;/strong&gt;. The code is TypeScript on Effect 4 and &lt;code&gt;amqplib&lt;/code&gt;, one branch per step:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/lsfera" rel="noopener noreferrer"&gt;
        lsfera
      &lt;/a&gt; / &lt;a href="https://github.com/lsfera/circuit-breaker-unboxing" rel="noopener noreferrer"&gt;
        circuit-breaker-unboxing
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Reasoning over circuit breaker
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Abstracting over the circuit breaker&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;📖 Read the full article: &lt;a href="https://lsfera.github.io/circuit-breaker-unboxing/" rel="nofollow noopener noreferrer"&gt;Circuit Breaker: Unboxing&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/lsfera/circuit-breaker-unboxing/blob/article/01-base-scenario/README.md" rel="noopener noreferrer"&gt;Start with 01 · Baseline(no breaker)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;"Add a circuit breaker" usually means adding a library to every service that calls
the flaky dependency. Written that way, the breaker looks like one thing. It is
really four answers that a library happens to give together:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What counts as a failure.&lt;/strong&gt; A &lt;code&gt;503&lt;/code&gt; is the third party failing. A &lt;code&gt;429&lt;/code&gt; is it
asking you to slow down. A &lt;code&gt;422&lt;/code&gt; is it refusing this one request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where the state lives.&lt;/strong&gt; In each process's memory, in something the fleet
shares, or in a control plane.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What "open" does.&lt;/strong&gt; It rejects calls locally, or it stops taking work at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who decides to try again.&lt;/strong&gt; A timer in every replica, or one probe the whole
fleet agrees on.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With the four pulled apart, each can be given a better answer than the…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lsfera/circuit-breaker-unboxing" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>rabbitmq</category>
      <category>architecture</category>
      <category>distributedsystems</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
