<?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: Kenneth Onyenuloya</title>
    <description>The latest articles on DEV Community by Kenneth Onyenuloya (@kenneth_onyenuloya).</description>
    <link>https://dev.to/kenneth_onyenuloya</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%2F4085570%2F70ca202d-1b59-431c-aa5c-56f56638ae11.png</url>
      <title>DEV Community: Kenneth Onyenuloya</title>
      <link>https://dev.to/kenneth_onyenuloya</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kenneth_onyenuloya"/>
    <language>en</language>
    <item>
      <title>Your CI Pipeline Is Making GitHub's Outages Worse. Here's How to Stop.</title>
      <dc:creator>Kenneth Onyenuloya</dc:creator>
      <pubDate>Wed, 19 Aug 2026 19:41:54 +0000</pubDate>
      <link>https://dev.to/kenneth_onyenuloya/-your-ci-pipeline-is-making-githubs-outages-worse-heres-how-to-stop-1012</link>
      <guid>https://dev.to/kenneth_onyenuloya/-your-ci-pipeline-is-making-githubs-outages-worse-heres-how-to-stop-1012</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19dc4yd3hr01ztlo650r.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19dc4yd3hr01ztlo650r.png" alt="Circuit breaker state machine diagram showing Closed, Open, and Half-Open states with labeled transitions: failure-threshold reached moves Closed to Open, cooldown elapsed moves Open to Half-Open, successful probes close the circuit, and a failed probe sends it back to Open" width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On August 17, 2026, GitHub went down for roughly 7.5 hours. Partway through the incident, GitHub's engineers made a decision that tells you everything about how outages actually spiral: they disabled authentication-token retries. Not added more capacity. Not scaled up. They turned retries &lt;em&gt;off&lt;/em&gt;, because the retries themselves were adding load to a system that was already struggling to stay up.&lt;/p&gt;

&lt;p&gt;That's the part of the story most postmortems gloss over, and it's worth sitting with. An outage doesn't just happen to a service — it gets amplified by everyone downstream of it, reacting the same way at the same time. Thousands of pipelines, all failing, all retrying, all adding more load to the exact thing that's already drowning.&lt;/p&gt;

&lt;p&gt;Your CI pipeline was almost certainly one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanic: why retries make things worse, not better
&lt;/h2&gt;

&lt;p&gt;Retry logic exists because most failures are transient — a dropped packet, a momentary timeout, a brief 503. Retry once or twice, it usually clears up, nobody notices. That's the theory, and for isolated failures it works.&lt;/p&gt;

&lt;p&gt;It stops working the moment the failure isn't isolated. If an upstream service is genuinely degraded, every client hitting it fails, and every client's retry logic kicks in at roughly the same time. Instead of one request, the struggling service now gets three, five, ten times the load — from retries alone. This is the "thundering herd" or "retry storm" pattern, and it's a well-known failure amplifier in distributed systems. GitHub's own mitigation on Aug 17 was a live demonstration of it.&lt;/p&gt;

&lt;p&gt;The fix that's been known in distributed systems for over a decade is the &lt;strong&gt;circuit breaker&lt;/strong&gt; pattern (Martin Fowler wrote the canonical description of it): stop calling a service that's clearly down, wait, then cautiously test if it's recovered before resuming full traffic. It's the software equivalent of the fuse box in your house — when something's wrong, cut the circuit rather than let it keep drawing power and making things worse.&lt;/p&gt;

&lt;p&gt;CI pipelines mostly don't do this. They retry hard, then give up, then the &lt;em&gt;next run&lt;/em&gt; does the exact same thing from scratch — with zero memory of the fact that this dependency has been failing for the last two hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's on the Marketplace today — and the actual gap
&lt;/h2&gt;

&lt;p&gt;I went looking for a GitHub Action that implements a real circuit breaker before building one. There's no shortage of retry actions — &lt;code&gt;nick-fields/retry&lt;/code&gt; and &lt;code&gt;Wandalen/wretry.action&lt;/code&gt; are both solid, and some already support jittered backoff delays. If your problem is "this one flaky step needs a couple of retries," those are the right tools.&lt;/p&gt;

&lt;p&gt;But none of them are circuit breakers, because none of them remember anything past the current job. Every run starts from zero. If a dependency has been down for two hours, run #47 retries into the same wall that runs #1 through #46 already hit, wastes its full retry budget, times out slowly, and fails — and so does #48. There's a GitHub community feature request asking for exactly this ("auto-pause a workflow that's failed 5 times in a row instead of burning through runner minutes") that's been sitting unfulfilled. That's the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry-Storm Circuit Breaker
&lt;/h2&gt;

&lt;p&gt;I built a GitHub Action that closes that gap: a circuit breaker that &lt;strong&gt;remembers failures across workflow runs&lt;/strong&gt;, not just within one job.&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Call flaky payments API&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;breaker&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kennethonyenuloya/retry-storm-breaker@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments-api&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;curl -sf https://payments.example.com/health&lt;/span&gt;
    &lt;span class="na"&gt;failure-threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;window-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;open-duration-seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
    &lt;span class="na"&gt;half-open-max-calls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a standard three-state breaker — Closed, Open, Half-Open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Closed&lt;/strong&gt;: normal operation, calls go through, failures are tracked in a rolling window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open&lt;/strong&gt;: after enough failures in that window, it stops making real calls entirely for a cooldown period and fails fast instead — no more slow timeouts, no more piling on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Half-Open&lt;/strong&gt;: after cooldown, it lets through a small number of probe calls. Succeed, and it closes back up. Fail, and it goes straight back to Open, cooldown reset.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting engineering problem — the reason this doesn't already exist as a trivial wrapper — is state persistence. GitHub's Actions cache API only lets you write a given key &lt;strong&gt;once&lt;/strong&gt;; a second save to the same key returns a 409 conflict. That's fine for caching &lt;code&gt;node_modules&lt;/code&gt;, useless for anything you need to &lt;em&gt;update&lt;/em&gt; every run.&lt;/p&gt;

&lt;p&gt;The workaround: save state under a timestamp-suffixed key every time (&lt;code&gt;cb-state-payments-api-2026-08-19T02:14:00Z&lt;/code&gt;), and restore using a prefix match instead of an exact key. GitHub's cache restore already falls back to the most recently created entry matching a prefix, so this gives mutable, "last write wins" state on top of an API that's fundamentally immutable — using only the default &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; cache permissions. No external database, no secrets to manage, no extra scopes to grant.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it deliberately doesn't do
&lt;/h2&gt;

&lt;p&gt;Worth being precise, because I don't want this to overclaim: &lt;strong&gt;this does not fix GitHub's outages.&lt;/strong&gt; Nothing running in your own workflow can. What it fixes is your side of the interaction — it stops your pipeline from being one more source of load piling onto a struggling dependency, and it stops your own CI minutes from being burned re-discovering the same outage every single run.&lt;/p&gt;

&lt;p&gt;It also doesn't ship Slack, Teams, or Discord notifications, on purpose. That space is already well served by mature, dedicated actions, and bundling notification logic here would mean taking on webhook and secret-handling liability for no real benefit over composing with what already exists. Instead, it exposes a &lt;code&gt;circuit-state&lt;/code&gt; output you branch on:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Notify Slack when breaker trips&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.breaker.outputs.circuit-state == 'open'&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rtCamp/action-slack-notify@v2&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;SLACK_WEBHOOK&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SLACK_WEBHOOK }}&lt;/span&gt;
    &lt;span class="na"&gt;SLACK_MESSAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Circuit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;breaker&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tripped:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;payments-api&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;down."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap in whatever notifier your team already trusts. The action's job is producing a clean signal, not owning your alerting stack.&lt;/p&gt;

&lt;p&gt;It's also scoped to a single step, not a whole workflow — auto-pausing an entire pipeline is a bigger blast-radius decision that needs its own permissions and its own safety guardrails, and is a legitimate future direction rather than something bolted onto v1.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this would have looked like on Aug 17
&lt;/h2&gt;

&lt;p&gt;It's easy to wave at "reduces load, saves CI minutes" without showing what that means. So walk through it concretely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without the breaker&lt;/strong&gt;, every run touching something behind GitHub's auth layer during those 7.5 hours would independently retry a few times, time out slowly, and fail. Then the next commit triggers another run. Same thing, again — for the full duration of the outage, every single run rediscovers the outage from scratch and pays the full cost of finding out: slow timeouts, burned CI minutes, and a PR list full of red checks with no signal of "this is a known outage" versus "your code broke something."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With the breaker wired around that step:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The first few runs still fail.&lt;/strong&gt; No circuit breaker has a crystal ball — it needs to actually see failures accumulate (say, 5 failures in a rolling window of 10) before it has evidence this is sustained trouble, not a one-off blip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Once it trips&lt;/strong&gt;, every subsequent run during the outage fails &lt;em&gt;instantly&lt;/em&gt;, with an explicit message: &lt;code&gt;Circuit breaker OPEN — skipping execution, cooldown in progress.&lt;/code&gt; A 2-second fail instead of a slow multi-minute timeout, repeated across however many runs your team would have triggered over 7.5 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you branch the &lt;code&gt;circuit-state&lt;/code&gt; output into a Slack notification&lt;/strong&gt;, the trip fires once — not once per failed run. The team finds out from a single clear message instead of a wall of unexplained red X's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The next time a run happens to execute that step&lt;/strong&gt; — triggered by a push, a scheduled cron, or a manual re-run, same as any other workflow trigger — the breaker checks whether the cooldown has elapsed and, if so, quietly probes once. Succeed, and it closes and resumes normal behavior for future runs. The breaker doesn't create runs or re-trigger anything itself; it just makes the right call, automatically, within whatever run happens to occur next — so nobody has to remember "GitHub's back up, go re-run that failed job."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What it would not have done:&lt;/strong&gt; made your pipeline succeed &lt;em&gt;during&lt;/em&gt; the outage. If a step genuinely depends on GitHub's API being up, no circuit breaker changes that — GitHub being down is still GitHub being down. What changes is the cost and clarity of that downtime: fast, obvious, single-notification failures instead of slow, repeated, confusing ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability, opt-in
&lt;/h2&gt;

&lt;p&gt;If you want to see breaker trips on a dashboard rather than just in Actions logs, set an OTLP endpoint and it'll emit a span per invocation (&lt;code&gt;ci.step.circuit_breaker&lt;/code&gt;, with state-transition attributes) to whatever collector you already run. Left empty by default — no network calls happen unless you explicitly turn it on.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kennethonyenuloya/retry-storm-breaker@v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Marketplace listing: &lt;code&gt;github.com/marketplace/actions/retry-storm-circuit-breaker&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If your pipeline calls anything outside your own infrastructure — a third-party API, a package registry, an internal service that occasionally has a bad day — this is worth two minutes to wire in before the next outage, not during it.&lt;/p&gt;

</description>
      <category>ci</category>
      <category>monitoring</category>
      <category>github</category>
    </item>
  </channel>
</rss>
