<?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: Kashif Manzer</title>
    <description>The latest articles on DEV Community by Kashif Manzer (@kashif_manzer).</description>
    <link>https://dev.to/kashif_manzer</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%2F4141917%2F22f6777f-adeb-4fe8-86bd-58fce72dfb78.png</url>
      <title>DEV Community: Kashif Manzer</title>
      <link>https://dev.to/kashif_manzer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kashif_manzer"/>
    <language>en</language>
    <item>
      <title>The API your automation switched on is the one the attacker needs</title>
      <dc:creator>Kashif Manzer</dc:creator>
      <pubDate>Wed, 14 Oct 2026 16:20:20 +0000</pubDate>
      <link>https://dev.to/kashif_manzer/the-api-your-automation-switched-on-is-the-one-the-attacker-needs-3af4</link>
      <guid>https://dev.to/kashif_manzer/the-api-your-automation-switched-on-is-the-one-the-attacker-needs-3af4</guid>
      <description>&lt;p&gt;On October 7, Cisco published a batch of security advisories for NX-OS, the operating system that runs its Nexus data center switches. Buried in the batch were five flaws that let an unauthenticated attacker on the network run code as root on the switch. No login. No credentials. Just crafted network traffic, and the box that carries your whole data center fabric is compromised.&lt;/p&gt;

&lt;p&gt;The part that stuck with me is not the severity. A CVSS 9.8 is a number we see every month. It is who turned the attack surface on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The locked door that ships locked
&lt;/h2&gt;

&lt;p&gt;Take NX-API first. NX-API is a JSON and XML interface over HTTP for managing the switch. Instead of typing commands into the CLI, your automation talks HTTP: Ansible playbooks, Terraform providers, SDN controllers, the CI pipeline pushing config at 2am. It is the switch's management API, built for machines, not humans.&lt;/p&gt;

&lt;p&gt;Cisco ships it disabled. On Nexus 3000 and 9000 switches, NX-API sits off by default, and nothing turns it on except a working network: someone enabling it so the tooling can do its job.&lt;/p&gt;

&lt;p&gt;CVE-2026-76471 is a heap buffer overflow in NX-API, reached through a crafted HTTP request. An attacker who can reach the management interface sends one bad request, and the parser that was built to serve your automation becomes the path to root on the device. There is no login step in the chain. The API was built for machines, and the attack is a machine sending bytes.&lt;/p&gt;

&lt;p&gt;A quick explainer, because the words matter. A buffer overflow means the program reads more data into a fixed-size memory region than the region can hold. The extra data spills into neighbouring memory. A heap buffer overflow means the spilling happens in the heap, the program's dynamic memory area. When that memory belongs to privileged code, the attacker can overwrite the program's own control structures and steer it. Steering privileged code means root.&lt;/p&gt;

&lt;p&gt;On the UCS 6300 fabric interconnects, the same CVE rides a different gate. There the vulnerable API is the UCS Manager XML API, enabled by default and impossible to switch off without breaking management. Exploitation there needs a valid low-privileged login, so Cisco rates that variant High rather than Critical. One CVE, two platforms, two different gates.&lt;/p&gt;

&lt;h2&gt;
  
  
  The monitoring plane is an attack surface too
&lt;/h2&gt;

&lt;p&gt;The second cluster lives in NGOAM, Next Generation Operation, Administration, and Maintenance. NGOAM is the feature that verifies path health on Nexus switches: it checks that the data center fabric is actually carrying traffic the way you think it is. It is monitoring infrastructure, the kind of thing you enable and forget.&lt;/p&gt;

&lt;p&gt;Three CVEs, each CVSS 9.8: CVE-2026-76485, CVE-2026-76486, CVE-2026-76501. All three are stack-based buffer overflows from improper input validation, reached with crafted IP packets sent to the switch. CVE-2026-76485 needs only NGOAM enabled. CVE-2026-76486 needs NGOAM plus SRv6 or an NV Overlay. CVE-2026-76501 needs NGOAM plus SRv6. (SRv6 is Segment Routing over IPv6, a way of steering traffic by encoding the path in the packet header. NV Overlay is network virtualization, the VXLAN-based overlay most data center fabrics run on.)&lt;/p&gt;

&lt;p&gt;The pattern repeats: a feature that exists to let the network be managed and observed is the one parsing untrusted input in privileged code.&lt;/p&gt;

&lt;h2&gt;
  
  
  No workarounds
&lt;/h2&gt;

&lt;p&gt;Cisco's position is unusually flat. For these advisories, the workaround section is effectively one sentence: there are none. No configuration change, no ACL, no knob that removes the exposure.&lt;/p&gt;

&lt;p&gt;The temporary bridge is a Live Protect shield for CVE-2026-76471, which Cisco describes as a stopgap until upgrades can be scheduled. The permanent fix is the upgrade you keep postponing. The NX-API advisory does not print version strings; it sends operators to the Cisco Software Checker for the first fixed release on each platform and version.&lt;/p&gt;

&lt;p&gt;At publication, Cisco's PSIRT said it was not aware of public announcements or malicious use of these flaws. The flaws surfaced during Cisco's internal security testing, which Cisco says combined its existing testing processes with frontier AI models. Make of that what you will. The bugs are real either way.&lt;/p&gt;

&lt;p&gt;One line of wider context: the same October 7 publication cycle also carried a License On-Prem advisory with a CVSS 10.0 signature verification flaw, CVE-2026-76482. Separate product, separate advisory, same day, same lesson.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which side of the line is your fleet on?
&lt;/h2&gt;

&lt;p&gt;The audit is one command. On any Nexus 3000 or 9000 in standalone NX-OS mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;show feature | include nxapi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it comes back enabled, that switch has the management-plane exposure until it is upgraded. For NGOAM, the question is whether the monitoring feature is on, and whether SRv6 or NV Overlay rides alongside it.&lt;/p&gt;

&lt;p&gt;The uncomfortable takeaway: this exposure is not a misconfiguration in the classic sense. Nobody typed a bad password or left a port open by accident. The features exist to let machines manage the switch, and the machine-centric workflow, your automation, is what enables them. Programmability is the attack surface. The safe default and the working network are the same decision, and right now the decision is an upgrade.&lt;/p&gt;

&lt;p&gt;When was the last time you checked which features your fleet has on that you did not personally enable?&lt;/p&gt;




&lt;p&gt;Further reading: Cisco's NGOAM advisory &lt;a href="https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-ngoam-rce-LWKQ4BU" rel="noopener noreferrer"&gt;cisco-sa-ngoam-rce-LWKQ4BU&lt;/a&gt;, the &lt;a href="https://forkast.news/cisco-nx-api-carries-an-unauthenticated-root-rce-and-it-stays-off-until-automation-turns-it-on/" rel="noopener noreferrer"&gt;Forkast deep dive on the NX-API flaw&lt;/a&gt;, and ThreatAft's &lt;a href="https://threataft.com/articles/cisco-october-2026-mega-roundup-ngoam-license-on-prem" rel="noopener noreferrer"&gt;full October 7 roundup&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>networking</category>
      <category>devops</category>
      <category>backend</category>
    </item>
    <item>
      <title>How to stop a cron job from running twice</title>
      <dc:creator>Kashif Manzer</dc:creator>
      <pubDate>Mon, 12 Oct 2026 16:11:04 +0000</pubDate>
      <link>https://dev.to/kashif_manzer/how-to-stop-a-cron-job-from-running-twice-1000</link>
      <guid>https://dev.to/kashif_manzer/how-to-stop-a-cron-job-from-running-twice-1000</guid>
      <description>&lt;h1&gt;
  
  
  How to stop a cron job from running twice
&lt;/h1&gt;

&lt;p&gt;You have a cron job that runs every minute. It takes 90 seconds to finish.&lt;/p&gt;

&lt;p&gt;That is the whole bug. Read it again. Cron does not check whether your last run finished. It fires on the schedule, blindly, every time. Minute one starts the job. Minute two starts a second copy while the first is still working. Minute three starts a third. They step on each other: double writes, duplicate emails, two workers chewing the same queue.&lt;/p&gt;

&lt;p&gt;Everyone learns this the hard way. The fix is one idea: the job must take a lock before it works, and walk away politely if the lock is already taken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch it happen
&lt;/h2&gt;

&lt;p&gt;Here is a tiny Python job. It grabs an exclusive lock, then works for 8 seconds. I ran three copies of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/demo.lock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOCK_EX&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOCK_NB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;BlockingIOError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s] another copy is running, exiting&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s] lock acquired, doing the work...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s] work done&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real terminal output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- instance 1 starts (foreground, holds lock 8s) ---
--- instance 2 starts while instance 1 holds the lock ---
[0.000s] another copy is running, exiting without doing the work
instance 2 exit code: 1
[0.000s] lock acquired, doing the real work...
[8.000s] work done
--- instance 3 starts after instance 1 finished ---
[0.000s] lock acquired, doing the real work...
[8.000s] work done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instance 2 noticed the lock in under a millisecond and left. Instance 3, starting after instance 1 finished, went straight to work. No queueing, no piling up, no double work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the lock cannot get stuck
&lt;/h2&gt;

&lt;p&gt;This is the part that makes it trustworthy. The lock is not a flag in your program. It lives in the kernel. &lt;code&gt;flock&lt;/code&gt; asks the Linux kernel to mark the file "locked by this process", and the kernel releases it the moment the process dies, for any reason.&lt;/p&gt;

&lt;p&gt;Kill the job with Ctrl-C? Lock released. The job crashes? Lock released. Someone runs &lt;code&gt;kill -9&lt;/code&gt; and the process never gets to clean up? Lock released. The kernel always holds the other end of the rope, so there is no such thing as a stale lock file to clean up afterwards.&lt;/p&gt;

&lt;p&gt;One honest caveat: this holds on a normal local disk. On a network file system, advisory locks can behave strangely depending on the mount and the protocol version. Keep your lock file on local disk and you sidestep the entire class of problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same trick in Go
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;

    &lt;span class="s"&gt;"golang.org/x/sys/unix"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OpenFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/tmp/report.lock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_CREATE&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_RDWR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"cannot open lock file:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;unix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fd&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="n"&gt;unix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOCK_EX&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;unix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOCK_NB&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"another copy is already running"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;runTheReport&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;&lt;code&gt;LOCK_EX&lt;/code&gt; means exclusive, &lt;code&gt;LOCK_NB&lt;/code&gt; means "do not wait, fail immediately". That second flag is the whole decision: waiting would just recreate the pileup, one job deep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The laziest version
&lt;/h2&gt;

&lt;p&gt;If the job is a shell script, you do not need code at all. The &lt;code&gt;flock&lt;/code&gt; command wraps any program:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/bin/flock &lt;span class="nt"&gt;-n&lt;/span&gt; /tmp/report.lock /opt/jobs/report.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-n&lt;/code&gt; means non-blocking: if the lock is taken, &lt;code&gt;flock&lt;/code&gt; exits immediately without running the script. One flag, overlaps gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which pattern fits your job?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Skip the overlap&lt;/strong&gt; (reports, syncs, backups): non-blocking lock, second copy exits. This is the common case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never skip, never overlap&lt;/strong&gt; (queue drains): blocking lock, second copy waits its turn. Use &lt;code&gt;LOCK_EX&lt;/code&gt; without &lt;code&gt;LOCK_NB&lt;/code&gt;, but set a timeout so a genuinely stuck job cannot jam the line forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overlap is fine&lt;/strong&gt; (stateless, idempotent workers): no lock at all. Just be sure the work really is safe to run twice, because one day it will.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cron fires blindly. That is not a bug in cron; it is the contract. The fix is eight lines and a file the kernel babysits for you.&lt;/p&gt;

&lt;p&gt;What is the most surprising thing a double-running cron job ever did to you?&lt;/p&gt;

</description>
      <category>python</category>
      <category>go</category>
      <category>devops</category>
      <category>backend</category>
    </item>
    <item>
      <title>CircleCI was not hacked. Its database filled a disk with its own diary.</title>
      <dc:creator>Kashif Manzer</dc:creator>
      <pubDate>Sun, 11 Oct 2026 16:12:46 +0000</pubDate>
      <link>https://dev.to/kashif_manzer/circleci-was-not-hacked-its-database-filled-a-disk-with-its-own-diary-5551</link>
      <guid>https://dev.to/kashif_manzer/circleci-was-not-hacked-its-database-filled-a-disk-with-its-own-diary-5551</guid>
      <description>&lt;h1&gt;
  
  
  CircleCI was not hacked. Its database filled a disk with its own diary.
&lt;/h1&gt;

&lt;p&gt;Tuesday, October 6. You push a commit the way you have a thousand times, and the pipeline does not start. You check your config, because the config is always the first suspect. Nothing changed. You check the status page. It is not you. It is everyone.&lt;/p&gt;

&lt;p&gt;Intermittent delays started around 13:30 UTC. From 15:30 to 20:30, pipeline and workflow and job starts were delayed across the platform. From 15:48 to 16:38, nothing started at all. Pipelines triggered from 16:02 onward failed. The UI went stale. Notifications stopped. Commit statuses never reached GitHub. For five hours, CircleCI could not reliably move work forward.&lt;/p&gt;

&lt;p&gt;Nobody broke in. No deploy went wrong. No data was lost. The database behind CircleCI's workflow orchestration filled up a disk with its own diary, and then it could not write another word.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diary every database keeps
&lt;/h2&gt;

&lt;p&gt;Behind every pipeline you trigger sits an orchestration service. It tracks the state of every pipeline, every workflow, every job, and hands ready jobs to the machines that run them. All of that state lives in a database.&lt;/p&gt;

&lt;p&gt;Databases keep a diary. Before a database applies any change to its actual data, it writes that change into a transaction log first. This is called write-ahead logging, and the idea is simple: if the power dies halfway through a write, the database replays the diary on restart and recovers every change. The log is the record of what happened, in order. It is what makes your database durable.&lt;/p&gt;

&lt;p&gt;CircleCI's database keeps its diary on a dedicated storage volume, a disk reserved just for these logs. The logs are archived continuously: copied somewhere durable, then the space is reused. That only works if archiving keeps up with writing.&lt;/p&gt;

&lt;p&gt;On Tuesday, routine maintenance ran against the database's largest tables. Big tables mean big changes, and big changes mean the diary fills up fast. The maintenance wrote transaction logs faster than the archiver could drain them. The dedicated volume filled to the top.&lt;/p&gt;

&lt;p&gt;A database with a full transaction-log volume cannot accept writes. There is nowhere to record the next change, so every change waits. And a workflow orchestrator whose database cannot accept writes cannot move any work forward. No new pipeline state, no new job state, nothing handed to the execution fleet.&lt;/p&gt;

&lt;p&gt;That is the whole incident in one sentence: the diary filled the disk, and the database went quiet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that made it worse for 50 minutes
&lt;/h2&gt;

&lt;p&gt;The engineers could see where this was going. A database that cannot write to its log volume will eventually lock itself into read-only mode, and then recovery gets much harder. So they moved the transaction logs to the database's main storage volume, where there was room.&lt;/p&gt;

&lt;p&gt;Moving a live database's transaction logs is not a copy and paste. It took the database offline for 50 minutes.&lt;/p&gt;

&lt;p&gt;Sit with that for a second, because this is the part that stings. The fix for "the database is about to stop" was "take the database down ourselves, on purpose, for almost an hour." They traded an uncontrolled failure for a controlled one, because the controlled one had a known end. That is the kind of decision that only looks obvious from the outside. From the inside, at 16:00 UTC with every pipeline on the platform stuck, it is a brave call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second act
&lt;/h2&gt;

&lt;p&gt;When the database came back, it processed work at about half its normal rate. Pipelines started moving again, but slowly, like traffic after a road reopens with one lane closed. At 18:19 UTC the engineers changed a database setting, and throughput recovered. Job start times were back to normal by 20:30 UTC. Customers whose jobs failed during the window were told to rerun them.&lt;/p&gt;

&lt;p&gt;CircleCI published the postmortem on October 7. It is refreshingly plain about the chain: routine maintenance, archive lag, a full volume, a forced move, a slow recovery. No villains. Just a diary that outran its archivist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The operator's checklist
&lt;/h2&gt;

&lt;p&gt;If you run a database with write-ahead logging, and you almost certainly do, this incident hands you three things to check on Monday:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Monitor archive lag, not just disk used.&lt;/strong&gt; "Disk 70% full" tells you nothing about whether archiving is keeping up. The number that matters is the gap between the newest log written and the newest log archived, and whether that gap is growing. Alert on the lag trend, not the disk gauge. Something shaped like this, with your own metric names:&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;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TransactionLogArchiveFallingBehind&lt;/span&gt;
  &lt;span class="c1"&gt;# Fire when the archiver has not kept up for 15 minutes straight.&lt;/span&gt;
  &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db_wal_archive_lag_seconds &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;300&lt;/span&gt;
  &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15m&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Log&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;archiver&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;falling&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;behind&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$labels.instance&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Throttle heavy maintenance on your biggest tables.&lt;/strong&gt; The maintenance that triggered this was routine. That is the point. Routine operations against your largest tables are the ones most likely to outrun your archiving. Run them in smaller batches, or run them while someone is watching the lag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Size and watch the log volume like it is its own blast radius.&lt;/strong&gt; A dedicated log volume is good design. It is also a single point of failure with a countdown on it. Know how fast it fills at your peak write rate, and make sure the person on call knows where that number lives before Tuesday afternoon.&lt;/p&gt;

&lt;p&gt;The diary metaphor is doing real work here: the mechanism that saves your database during a crash is the same mechanism that takes it down when nobody is watching the archivist.&lt;/p&gt;

&lt;p&gt;What is the equivalent in your stack? The safety mechanism that quietly becomes the failure mode when it falls behind?&lt;/p&gt;

</description>
      <category>devops</category>
      <category>backend</category>
      <category>database</category>
      <category>networking</category>
    </item>
    <item>
      <title>GitHub Is Failing Your macOS Builds on Purpose This Month</title>
      <dc:creator>Kashif Manzer</dc:creator>
      <pubDate>Fri, 09 Oct 2026 16:11:19 +0000</pubDate>
      <link>https://dev.to/kashif_manzer/github-is-failing-your-macos-builds-on-purpose-this-month-b58</link>
      <guid>https://dev.to/kashif_manzer/github-is-failing-your-macos-builds-on-purpose-this-month-b58</guid>
      <description>&lt;h1&gt;
  
  
  GitHub Is Failing Your macOS Builds on Purpose This Month
&lt;/h1&gt;

&lt;p&gt;Monday morning. You open your laptop and your CI is red.&lt;/p&gt;

&lt;p&gt;Nobody shipped anything over the weekend. The diff is empty. You re-run the workflow, and it fails again. You re-run it once more for luck, and it fails a third time. Then, late in the afternoon, with no changes at all, it turns green.&lt;/p&gt;

&lt;p&gt;Your first instinct is to blame the cache. Your second instinct is to blame yourself. Both are wrong.&lt;/p&gt;

&lt;p&gt;GitHub did this on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a brownout is
&lt;/h2&gt;

&lt;p&gt;GitHub is retiring the macOS 14 runner image on November 2, 2026. That is the Sonoma-based virtual machine your workflow gets when the job says &lt;code&gt;runs-on: macos-14&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before the image goes away, GitHub is running brownouts: scheduled windows where every job on the macOS 14 labels fails on purpose. Not because something is broken. To make something visible.&lt;/p&gt;

&lt;p&gt;A brownout is the opposite of an outage. An outage is failure you did not plan. A brownout is failure you scheduled, announced, and documented, because you are more scared of the quiet kind. The quiet kind is a workflow that pins an old image and nobody notices until the retirement date, when it fails for real and there is no window left to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The schedule
&lt;/h2&gt;

&lt;p&gt;There are eight windows, each ten hours, 14:00 UTC to 00:00 UTC:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Oct 5 and 6 (this already happened)&lt;/li&gt;
&lt;li&gt;Oct 12 and 13&lt;/li&gt;
&lt;li&gt;Oct 16 and 17&lt;/li&gt;
&lt;li&gt;Oct 19 and 20&lt;/li&gt;
&lt;li&gt;Oct 23 and 24&lt;/li&gt;
&lt;li&gt;Oct 26 and 27&lt;/li&gt;
&lt;li&gt;Oct 29 and 30&lt;/li&gt;
&lt;li&gt;Oct 30 and 31&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Pacific time that is 7:00am to 5:00pm. Most of the American workday falls inside the failure window. The windows cluster toward the end of the month, with two of them back to back, which is the part where patience runs out.&lt;/p&gt;

&lt;p&gt;Inside a window, any job on &lt;code&gt;macos-14&lt;/code&gt;, &lt;code&gt;macos-14-large&lt;/code&gt;, or &lt;code&gt;macos-14-xlarge&lt;/code&gt; fails deliberately. It does not matter that your workflow is fine. The failure is the message.&lt;/p&gt;

&lt;p&gt;This applies to GitHub Actions and to Azure DevOps, because Microsoft-hosted agents are built from the same images.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the scheduled builds get hit first
&lt;/h2&gt;

&lt;p&gt;Here is the cruel part of the design. The builds that turn red first are the ones nobody is watching.&lt;/p&gt;

&lt;p&gt;Cron-scheduled workflows, nightly builds, matrix jobs that run across a fleet of repos at midnight: they fire right inside the window, and the first anyone hears about it is a red badge in the morning. A developer pushing at 10am sees the failure immediately and starts debugging. A scheduled job failing at 2am just sits there, red and unexamined, until standup.&lt;/p&gt;

&lt;p&gt;That asymmetry is exactly why GitHub does it this way. A brownout that only ever failed while humans were watching would be ignorable. One that fills your mornings with red across repos you forgot about is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quieter killer: the queue
&lt;/h2&gt;

&lt;p&gt;There is a second, sneakier effect, and it runs even outside the windows. Since the deprecation was announced in July, GitHub has been allowed to reduce capacity for the macOS 14 runners. Fewer machines serving the same jobs means longer queue times.&lt;/p&gt;

&lt;p&gt;So your macOS 14 jobs might not just fail in bursts. They might also sit waiting in the queue, for no reason your workflow file explains, getting slower and flakier all month. If your builds have been mysteriously slow since summer, this is probably why, and the brownouts are just the part GitHub put on a calendar.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is one search away
&lt;/h2&gt;

&lt;p&gt;Find every workflow that still asks for the old image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"macos-14"&lt;/span&gt; .github/workflows/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then move each one to a supported label:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macos-15&lt;/span&gt;   &lt;span class="c1"&gt;# or macos-latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub's named migration targets are &lt;code&gt;macos-latest&lt;/code&gt; (currently macos-26), &lt;code&gt;macos-15&lt;/code&gt;, and their &lt;code&gt;-xlarge&lt;/code&gt; counterparts.&lt;/p&gt;

&lt;p&gt;One warning before you swap the label and walk away. This is a real OS jump, not a version bump. Sonoma to Sequoia or newer means different Xcode and toolchain versions, different Homebrew and Clang versions, and anything your build quietly pinned to Sonoma behavior. Treat the first green run on the new image as suspicious. Run the full suite, check the artifacts, and re-verify anything that smells like it depended on the old machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern worth keeping
&lt;/h2&gt;

&lt;p&gt;Step back from the calendar for a second. What GitHub is doing here is chaos engineering with a courtesy notice. The industry spent a decade learning that the only way to find out what depends on a thing is to take the thing away on a schedule you control, rather than wait for it to disappear on a schedule you do not.&lt;/p&gt;

&lt;p&gt;Netflix kills its own servers in the middle of the day for the same reason. GitHub kills your builds for ten hours at a time for the same reason. The lesson generalizes: every dependency you cannot name is a brownout you did not schedule.&lt;/p&gt;

&lt;p&gt;November 2 is the real deadline. The brownouts are just the rehearsal, and the next rehearsal starts Monday at 7:00am Pacific.&lt;/p&gt;

&lt;p&gt;So here is my question for you: what in your stack is still pinned to macOS 14, and why?&lt;/p&gt;

</description>
      <category>github</category>
      <category>devops</category>
      <category>ci</category>
      <category>backend</category>
    </item>
    <item>
      <title>Your plan says (known after apply). OpenTofu 1.13 lets you talk back</title>
      <dc:creator>Kashif Manzer</dc:creator>
      <pubDate>Wed, 07 Oct 2026 16:10:02 +0000</pubDate>
      <link>https://dev.to/kashif_manzer/your-plan-says-known-after-apply-opentofu-113-lets-you-talk-back-17hf</link>
      <guid>https://dev.to/kashif_manzer/your-plan-says-known-after-apply-opentofu-113-lets-you-talk-back-17hf</guid>
      <description>&lt;p&gt;Every person who has reviewed an infrastructure plan knows the feeling.&lt;/p&gt;

&lt;p&gt;You run &lt;code&gt;tofu plan&lt;/code&gt; on a Friday afternoon. The output scrolls past. A third of the values you actually care about say the same thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;known&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="nx"&gt;apply&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So you squint. You reason from experience. You say "yes, that looks right" to a plan that is, in the strictest sense, guessing. Then you apply, and the guessing continues on your side until the resources exist.&lt;/p&gt;

&lt;p&gt;OpenTofu 1.13 shipped on September 30 (with a 1.13.1 patch on October 1, security support to August 2027) and it does something about that. It gives you a way to tell the planner what you already know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the plan can't know
&lt;/h2&gt;

&lt;p&gt;First, the mechanism, because it explains everything else.&lt;/p&gt;

&lt;p&gt;OpenTofu's plan is a prediction. It walks your configuration, calls the providers, and works out what would change without changing anything. That last part matters: it cannot call the cloud API to actually create things, because creating things is the whole point of apply.&lt;/p&gt;

&lt;p&gt;And that is where the unknowns come from. Plenty of values only exist after the remote system acts. An instance's ID, an ARN, the IP a load balancer will be assigned: these are chosen by the cloud API during apply. At plan time, OpenTofu has no way to compute them, so it marks them &lt;code&gt;(known after apply)&lt;/code&gt; and does the best it can with the rest.&lt;/p&gt;

&lt;p&gt;This is honest engineering. The problem is that unknowns poison everything they touch. If a value is unknown, any expression built on it is usually unknown too. Compare an unknown VPC ID to null and the result is unknown. Feed it into another module and the plan for that module goes murky. One unknown upstream turns into a wall of unknowns downstream, and you are back to squinting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tell the planner what you know
&lt;/h2&gt;

&lt;p&gt;Here is the new idea. You, the module author, often know things the provider cannot promise.&lt;/p&gt;

&lt;p&gt;Take a VPC ID from the AWS provider. During plan it is unknown. But you know something about it: it will never be null, and every real VPC ID starts with &lt;code&gt;vpc-&lt;/code&gt;. The provider's API documentation guarantees this. OpenTofu just had no way to hear it.&lt;/p&gt;

&lt;p&gt;The 1.13 &lt;code&gt;assume...&lt;/code&gt; family of functions is a megaphone for exactly that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"vpc_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;assumenotnull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;assumestringprefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"vpc-"&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;Read it as a sentence: "I assume this is not null, and I assume it is a string starting with vpc-."&lt;/p&gt;

&lt;p&gt;Now any expression that compares this output to null or to the empty string gets a known answer at plan time (false), instead of an unknown. If a downstream module validates that the VPC ID starts with &lt;code&gt;vpc-&lt;/code&gt;, that validation now runs during the plan phase, where you can see the mistake before anything is created.&lt;/p&gt;

&lt;p&gt;Two more worth knowing: &lt;code&gt;convert&lt;/code&gt; tells OpenTofu the expected type of a value it cannot infer (for example, the result of &lt;code&gt;jsondecode&lt;/code&gt; on something dynamic), so downstream attribute access gets a known type. And &lt;code&gt;assumeequal&lt;/code&gt; declares the expected final value outright.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch: your promise gets checked
&lt;/h2&gt;

&lt;p&gt;This is the part that matters most, so say it plainly.&lt;/p&gt;

&lt;p&gt;The planner takes your word for it during plan. But at apply time, once the real values arrive, OpenTofu checks. If you promised that an unknown string starts with &lt;code&gt;vpc-&lt;/code&gt; and it turns out to start with &lt;code&gt;subnet-&lt;/code&gt;, the &lt;code&gt;assumestringprefix&lt;/code&gt; call fails during the apply.&lt;/p&gt;

&lt;p&gt;That is the right design. An unchecked assumption would turn the plan into fiction. This way the hint is verified by reality, and the cost of lying to the planner is a failed apply rather than silent breakage.&lt;/p&gt;

&lt;p&gt;The rule of thumb: only promise what the provider or the vendor API documentation actually guarantees. "The docs say VPC IDs always look like this" is a promise. "In my experience they usually look like this" is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few other things in 1.13
&lt;/h2&gt;

&lt;p&gt;This was a short release cycle. The team cut it short on purpose to line up with the Go release schedule, which stretches the security support window to August 2027. So besides the unknown-value work, there are a couple of early experiments and some housekeeping:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Built-in linting, first pass.&lt;/strong&gt; &lt;code&gt;tofu plan -lint=all&lt;/code&gt; runs four rules: variables missing a type, &lt;code&gt;count&lt;/code&gt; used where the newer &lt;code&gt;enabled&lt;/code&gt; meta-argument fits, unused variables, unused locals. Very early. Expect it to grow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symbol Libraries, experimental.&lt;/strong&gt; A way to share values, functions, and type aliases across configurations, separate from modules. The shape is still evolving.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows on ARM64&lt;/strong&gt; is now an official platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Before you upgrade:&lt;/strong&gt; &lt;code&gt;base64gzip&lt;/code&gt; output changed slightly, which can make OpenTofu propose diffs on resources that use it. WinRM support for provisioners is gone (use OpenSSH for Windows). This is the last series with official 32-bit builds, and macOS builds now need Ventura or newer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this is bigger than it looks
&lt;/h2&gt;

&lt;p&gt;The quiet problem with &lt;code&gt;(known after apply)&lt;/code&gt; was never just aesthetics. Unknown values blind automated checks. Some teams run the machine-readable plan through policy-checking software, and every unknown forces a choice: trust optimistically and risk a violation, or block conservatively and hold up changes that would have been fine.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;assume...&lt;/code&gt; functions give module authors a way to sharpen the plan's vision, one honest guarantee at a time. They do not remove uncertainty. They let you say, precisely, where the uncertainty ends.&lt;/p&gt;

&lt;p&gt;Have you ever been burned by approving a plan full of unknowns? What was the one that got you?&lt;/p&gt;

</description>
      <category>opentofu</category>
      <category>terraform</category>
      <category>devops</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Codecov went down today over an expired certificate: how TLS renewal actually fails</title>
      <dc:creator>Kashif Manzer</dc:creator>
      <pubDate>Mon, 05 Oct 2026 16:14:28 +0000</pubDate>
      <link>https://dev.to/kashif_manzer/codecov-went-down-today-over-an-expired-certificate-how-tls-renewal-actually-fails-4k20</link>
      <guid>https://dev.to/kashif_manzer/codecov-went-down-today-over-an-expired-certificate-how-tls-renewal-actually-fails-4k20</guid>
      <description>&lt;h1&gt;
  
  
  Codecov went down today over an expired certificate: how TLS renewal actually fails
&lt;/h1&gt;

&lt;p&gt;This morning, Codecov's status page lit up. Frontend and backend, both down. The cause, identified within two minutes of the first update: an expired SSL certificate. The &lt;a href="https://pingoru.io/providers/codecov/incidents/12184032" rel="noopener noreferrer"&gt;incident timeline&lt;/a&gt;, which begins at 06:53 UTC today, goes from "investigating" to "identified: SSL certificate expiration" in two updates.&lt;/p&gt;

&lt;p&gt;Yesterday it was Sprinklr. Same failure. An &lt;a href="https://pingoru.io/providers/sprinklr/incidents/12183558" rel="noopener noreferrer"&gt;expired certificate&lt;/a&gt; knocked part of their production offline for 23 minutes before the team renewed and redeployed it.&lt;/p&gt;

&lt;p&gt;Two certificate-expiry outages in 24 hours. And this is not a rare disease. This is the common cold of web operations, and it keeps hospitalizing serious companies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your servers are fine. That is the problem.
&lt;/h2&gt;

&lt;p&gt;Here is the strange part of every certificate outage: nothing is actually broken on the server side. The app is running. The database is answering. Every internal health check is green.&lt;/p&gt;

&lt;p&gt;The failure happens inside the visitor's browser, in the split second before any page loads. The browser asks for the certificate, sees the expiry date has passed, and refuses to continue. Your monitoring watched the server. The server was never the patient.&lt;/p&gt;

&lt;p&gt;This is why the outage always feels so disorienting. The team stares at green dashboards while users stare at a red warning page. Two different realities, and the user's one is the only one that counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hall of fame nobody wants to join
&lt;/h2&gt;

&lt;p&gt;The roll call of serious companies taken down by this is long enough to be embarrassing.&lt;/p&gt;

&lt;p&gt;In December 2018, an expired certificate at Ericsson knocked out mobile data for tens of millions of O2 and SoftBank customers across the UK and Japan. O2 users were offline for most of the day, and the damage settlement that followed was reported at around 100 million pounds (&lt;a href="https://www.scworld.com/news/amid-high-profile-outages-automated-certificate-management-offers-a-solution" rel="noopener noreferrer"&gt;source&lt;/a&gt;, &lt;a href="https://www.manageengine.com/key-manager/blog/one-expired-security-certificate-leaves-millions-smartphones-offline.html" rel="noopener noreferrer"&gt;source&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;In February 2020, Microsoft Teams went down for roughly three hours on an expired certificate. Spotify lost about an hour the same year when a TLS certificate lapsed, and the team only found it after a Cloudflare engineer spotted the expired cert (&lt;a href="https://www.scworld.com/news/amid-high-profile-outages-automated-certificate-management-offers-a-solution" rel="noopener noreferrer"&gt;source&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;LinkedIn let a certificate on its lnkd.in shortener expire twice, in 2017 and again in 2019. The second time, the renewed certificate had been issued days earlier. It just never got installed on the server (&lt;a href="https://www.computerweekly.com/news/252463885/Lapse-in-LinkedIn-security-certificate-update" rel="noopener noreferrer"&gt;source&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Every one of these teams had smart people and real infrastructure. The certificate still won.&lt;/p&gt;

&lt;h2&gt;
  
  
  How renewal is supposed to work
&lt;/h2&gt;

&lt;p&gt;A quick primer, because the failure modes only make sense once you see the machinery.&lt;/p&gt;

&lt;p&gt;A TLS certificate is a file with an expiry date stamped inside it. Browsers trust it only between its start date and its end date. Public certificates used to last a bit over a year. Now the industry is shrinking that: the CA/Browser Forum cut the maximum validity from 398 days to 200 days starting March 2026, so certificates issued that March started expiring this month (&lt;a href="https://d33gy59ovltp76.cloudfront.net/news/why-october-1-2026-could-be-the-day-ssl-tls-certificates-break-the-internet" rel="noopener noreferrer"&gt;industry reporting&lt;/a&gt;). That is the wave we are standing in right now.&lt;/p&gt;

&lt;p&gt;Renewal is meant to be automatic. The standard protocol is called ACME. Your server proves it controls the domain, the certificate authority issues a fresh certificate, and a small agent on your machine installs it. Let's Encrypt made this free and ubiquitous. In Kubernetes, a tool called cert-manager does the same job for cluster workloads.&lt;/p&gt;

&lt;p&gt;There are two common ways to prove domain control: answer a challenge file over HTTP (called http-01), or create a DNS record (dns-01). Then the agent reloads the web server so the new certificate actually gets served.&lt;/p&gt;

&lt;p&gt;That last step is where the bodies are buried.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five ways renewal silently fails
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Renewed, but never reloaded.&lt;/strong&gt; The new certificate lands on disk. The old one stays in the server's memory. Nginx, Apache, or your Go service keeps serving the expired file until someone restarts or reloads the process. This is almost certainly what happened to LinkedIn in 2019: the renewed certificate existed, it just was not the one being served.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The copy you forgot.&lt;/strong&gt; Modern setups terminate TLS in several places: the CDN edge, the load balancer, the origin server. Renewal automation often covers exactly one of these. The edge gets a fresh certificate while the origin's copy quietly ages out, or the other way around. Every layer you did not automate is a timer you are not watching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The DNS token died quietly.&lt;/strong&gt; DNS-01 challenges need an API token for your DNS provider. Tokens get rotated, scopes get narrowed, the person who created it leaves the company. The renewal job starts failing, and it fails in the one place nobody reads: the output of a cron job. Sixty days of silent failures, then the expiry date arrives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The root you did not know you depended on.&lt;/strong&gt; Sometimes it is not your certificate that expires but the root it chains to. When Let's Encrypt's old cross-signed root expired, apps like Shopify and Slack saw outages on older devices that did not trust the new root (&lt;a href="https://www.techradar.com/news/slack-down-top-websites-see-outages-as-lets-encrypts-ca-certificate-expires" rel="noopener noreferrer"&gt;source&lt;/a&gt;). Your certificate can be perfectly fresh and still untrusted, because trust is a chain and chains have more than one link.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Monitoring that checks the wrong thing.&lt;/strong&gt; Most uptime checks hit an HTTP endpoint or a health port. Those pass with an expired certificate. The check that catches this has to complete a TLS handshake from the outside and read the expiry date. Almost nobody's default monitoring does that, so the first alert is always a user.&lt;/p&gt;

&lt;p&gt;None of these are exotic. Every one of them is a Tuesday.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checker that reads the expiry from the outside
&lt;/h2&gt;

&lt;p&gt;The fix is boring, which is why it works. Check every public endpoint from the outside, read the certificate's expiry date, and page someone while there are still weeks left, not minutes.&lt;/p&gt;

&lt;p&gt;There is one subtlety that bites people: a normal HTTPS client refuses to talk to a server with an expired certificate, so your checker has to deliberately skip validation. It is not checking identity here, only reading the date. Without that, the handshake fails on an expired cert and you learn nothing.&lt;/p&gt;

&lt;p&gt;Here is the whole thing in Go, using only the standard library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"crypto/tls"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// daysLeft opens a TLS connection the way a browser would,&lt;/span&gt;
&lt;span class="c"&gt;// reads the certificate's expiry date, and reports how many&lt;/span&gt;
&lt;span class="c"&gt;// days are left. InsecureSkipVerify is deliberate: we are not&lt;/span&gt;
&lt;span class="c"&gt;// checking identity here, only reading the date.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;daysLeft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;":443"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;InsecureSkipVerify&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;certs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PeerCertificates&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;certs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"server presented no certificates"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;expiry&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;certs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotAfter&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Until&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hours&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;domains&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"api.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"status.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;domains&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;daysLeft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%-20s CHECK FAILED: %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"ok"&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"RENEW SOON"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"EXPIRED"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%-20s %s: %d days left (expires %s)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expiry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2006-01-02"&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;Put your real domains in the list, not just the marketing site. The API subdomain, the status page, the webhook receiver, the admin panel. The certificate that takes you down is always the one you forgot you had.&lt;/p&gt;

&lt;p&gt;Then add the alerting rule that actually works: warn at 30 days, page at 14, treat 7 as an incident. A certificate is free to renew. There is no reason to ever be surprised by its expiry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wave is just starting
&lt;/h2&gt;

&lt;p&gt;Back to the timing, because it matters. With maximum validity now at 200 days, every team renews roughly twice as often as they used to. Twice as many renewal windows, twice as many chances for one of the five silent failures above. The teams that treated renewal as "set and forget" are about to learn what "forget" costs, twice a year, on a schedule.&lt;/p&gt;

&lt;p&gt;Codecov and Sprinklr, 24 hours apart, might be the first drops of that rain. Or they might be coincidence. Either way, the mechanism does not care about the calendar. It only cares whether someone is watching the expiry date from the outside.&lt;/p&gt;

&lt;p&gt;Check yours today. It takes five minutes, and the checker above is most of the work already done.&lt;/p&gt;

&lt;p&gt;What is the most embarrassing cause of an outage you have ever seen? I want to hear the one that made the whole team go quiet.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>networking</category>
      <category>backend</category>
    </item>
    <item>
      <title>A broken cable on the ocean floor slowed the internet for a whole country</title>
      <dc:creator>Kashif Manzer</dc:creator>
      <pubDate>Sun, 04 Oct 2026 16:10:23 +0000</pubDate>
      <link>https://dev.to/kashif_manzer/a-broken-cable-on-the-ocean-floor-slowed-the-internet-for-a-whole-country-1a4j</link>
      <guid>https://dev.to/kashif_manzer/a-broken-cable-on-the-ocean-floor-slowed-the-internet-for-a-whole-country-1a4j</guid>
      <description>&lt;h1&gt;
  
  
  A broken cable on the ocean floor slowed the internet for a whole country
&lt;/h1&gt;

&lt;p&gt;Friday, October 2, 2026. All over the Philippines, people noticed the same thing: the internet was slow. Videos buffered. Pages took their time. Calls stuttered.&lt;/p&gt;

&lt;p&gt;It was not their routers. It was not their ISP throttling them. It was a broken cable lying on the bottom of the sea.&lt;/p&gt;

&lt;p&gt;By that evening, the advisories were out. Converge, PADECO, even the network help desk at UP Diliman, all warned subscribers about the same thing: the submarine cable segments connecting the Philippines to Singapore and Hong Kong were down. The country's Department of Information and Communications Technology said it was investigating the cause. Sabotage was on the list of possibilities, but officials said it was far too early to draw conclusions.&lt;/p&gt;

&lt;p&gt;And then this quiet detail from PLDT, one of the country's biggest carriers: it had lost about 300 gigabits per second of capacity, and it had more than enough spare elsewhere to cover the loss.&lt;/p&gt;

&lt;p&gt;That one sentence contains almost the entire story of how the internet actually works. Let me unpack it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The internet is a maritime business
&lt;/h2&gt;

&lt;p&gt;Here is the fact that surprises almost everyone: when your data travels to another continent, it almost certainly goes through the ocean. Submarine cables carry over 99 percent of all intercontinental data traffic, according to TeleGeography, the research firm that maps them. Satellites handle a tiny fraction. They always have.&lt;/p&gt;

&lt;p&gt;There are nearly 700 cable systems down there, with about 1,900 spots where they come ashore. Each cable is roughly as thick as a garden hose. Inside are strands of glass, thinner than a human hair, and data travels through them as pulses of light. Small devices called repeaters, spaced along the cable, boost the signal so it survives the thousands of kilometers between continents.&lt;/p&gt;

&lt;p&gt;And these cables break. A lot. Roughly 150 to 200 times a year worldwide. The cause is usually mundane: a ship's anchor dragging across the seabed, a fishing trawler's gear snagging a line, an earthquake, or plain equipment failure. The ocean floor is crowded and violent.&lt;/p&gt;

&lt;p&gt;So why is the internet not breaking every week? Because it was built assuming this would happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why these two cables mattered
&lt;/h2&gt;

&lt;p&gt;Singapore and Hong Kong are not random dots on the map. They are two of Asia's biggest internet intersections. Networks from all over the region meet there to exchange traffic, because it is cheaper and faster to hand data to a neighbor than to send it the long way around. Engineers call these meeting points hubs, and the practice of exchanging traffic there is called peering.&lt;/p&gt;

&lt;p&gt;A lot of the Philippines' traffic to the rest of the world normally passes through those two hubs. When the segments to both went down at once, it was like losing two major interchanges on the same day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the internet got slow instead of dying
&lt;/h2&gt;

&lt;p&gt;Here is the key question: if the cables broke, why did anything work at all?&lt;/p&gt;

&lt;p&gt;Because the internet does not trust any single road. Philippine ISPs lease capacity on many cable systems heading in different directions: toward Hong Kong, Japan, Guam, the US west coast. When two paths died, traffic moved to the rest.&lt;/p&gt;

&lt;p&gt;Every network on the internet constantly tells its neighbors which routes are alive. The system they use for this is called BGP, the Border Gateway Protocol. You can think of it as every intersection in the world posting live detour signs. A router in Manila says, "I can reach Singapore through this path." When the path dies, the announcement is withdrawn, the signs update everywhere within minutes, and traffic finds the next best route.&lt;/p&gt;

&lt;p&gt;This is why slow, not dead. Picture a highway losing two lanes at rush hour. The same number of cars now squeezes through the lanes that remain. That is congestion. And some of the surviving routes are physically longer: instead of Manila to Singapore to its destination, a packet might travel Manila to Tokyo to the US west coast and back. Light is fast, but distance still counts. The delay has a name every backend engineer knows: latency. A few hundred extra milliseconds is the difference between a snappy call and a buffering one.&lt;/p&gt;

&lt;p&gt;Globe, one of the country's big carriers, announced it was rerouting data traffic. That sentence is the press-release version of "we updated the detour signs."&lt;/p&gt;

&lt;p&gt;A simplified sketch of what happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Manila ====== Singapore ====== rest of the world     X  broken
Manila ====== Hong Kong  ===== rest of the world     X  broken
Manila ====== Tokyo ====== rest of the world            longer, congested
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same cars. Fewer lanes. Longer detours.&lt;/p&gt;

&lt;h2&gt;
  
  
  How they find the break
&lt;/h2&gt;

&lt;p&gt;This is my favorite part. When a cable snaps, the operator does not send a diver. They send a pulse of light down the fiber and listen for the echo.&lt;/p&gt;

&lt;p&gt;It works like sonar. A break in the glass reflects a tiny bit of the light back. By measuring how long the echo takes to return, engineers can pinpoint the break to within meters, from a control room thousands of kilometers away. The tool has a fancy name, an optical time-domain reflectometer, but the idea is ancient: shout into a canyon, time the echo, know the distance.&lt;/p&gt;

&lt;p&gt;Then a cable ship sails to the spot. It drags a grappling hook, or sends down an underwater robot, to haul the cable up from the seabed. Technicians splice in a new section of fiber, seal it, and drop it back down. It takes days to weeks, depending on weather and how far the nearest ship is. There are only about two dozen of these ships on standby around the world.&lt;/p&gt;

&lt;p&gt;And while the ship is sailing, the internet just routes around the hole. You feel it as slowness. Your video call has no idea why. All it knows is that the road got longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "300 gigabits of capacity" actually means
&lt;/h2&gt;

&lt;p&gt;Back to PLDT's quiet sentence. ISPs do not own most of these cables. They buy capacity: slices of a cable's total throughput, like an airline buying seats on a route it does not own. Three hundred gigabits per second is roughly the room for tens of thousands of simultaneous video streams. Losing it is a real hit.&lt;/p&gt;

&lt;p&gt;The second half of the sentence is the interesting part: "we have more than enough to cover it." That is redundancy, and it is the most boring and most important word in infrastructure. PLDT had paid for spare capacity on other routes, every day, for years, for exactly this moment. Redundancy is insurance you buy daily and are grateful for on the worst day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The internet feels like weather, something in the air. It is not. It is glass tubes on the ocean floor, a small fleet of repair ships, and routers swapping detour signs with each other thousands of times a second.&lt;/p&gt;

&lt;p&gt;Every time your connection slows and nobody can tell you why, remember the Philippines on October 2. Somewhere under the sea, possibly, a ship's anchor did it.&lt;/p&gt;

&lt;p&gt;Here is a small experiment for this week. Run &lt;code&gt;traceroute&lt;/code&gt; (or &lt;code&gt;mtr&lt;/code&gt; on Linux, &lt;code&gt;tracert&lt;/code&gt; on Windows) against a site you use every day and read the city names in the hops. The journey is more literal than you think. What is the strangest route your packets have ever taken? Tell me in the comments.&lt;/p&gt;

</description>
      <category>networking</category>
      <category>devops</category>
      <category>backend</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Why your retries need jitter: the thundering herd, explained</title>
      <dc:creator>Kashif Manzer</dc:creator>
      <pubDate>Fri, 02 Oct 2026 16:10:14 +0000</pubDate>
      <link>https://dev.to/kashif_manzer/why-your-retries-need-jitter-the-thundering-herd-explained-3eje</link>
      <guid>https://dev.to/kashif_manzer/why-your-retries-need-jitter-the-thundering-herd-explained-3eje</guid>
      <description>&lt;p&gt;Picture this. It is 2:14 in the morning and your checkout service starts returning errors. Every request fails for about twenty seconds, then your deploy finishes rolling back and the error rate drops to zero. You exhale.&lt;/p&gt;

&lt;p&gt;Then the dashboards spike again. Harder than before. Nothing changed on your end. The service was healthy. So what knocked it over the second time?&lt;/p&gt;

&lt;p&gt;Your own services did. Every client that failed during those twenty seconds had been retrying, and the moment the service came back, they all arrived at once. The recovery got flattened by a second wave made entirely of retries. That second wave has a name engineers have used for decades: the thundering herd.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive retry
&lt;/h2&gt;

&lt;p&gt;The first retry loop most of us write looks like this: call the service, and if it fails, try again. Immediately. Maybe three times.&lt;/p&gt;

&lt;p&gt;For one client, that is harmless. But failures in distributed systems are rarely lonely. A deploy, a network blip, a database failing over: these knock out hundreds or thousands of clients in the same second. Now all of them retry in the same second too. The service comes back to life and instantly receives ten times its normal traffic, all at once, and falls over again. Your retry logic just turned a twenty second outage into a rolling one.&lt;/p&gt;

&lt;p&gt;(A quick term check: a distributed system is just a bunch of services talking to each other over a network instead of one big program. The network between them is the part that fails in interesting ways.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixed delays do not break the herd
&lt;/h2&gt;

&lt;p&gt;The obvious fix is to wait between retries. Retry every five seconds, say. That helps the service, but the clients are still marching in lockstep. They all failed together, so they all wait five seconds together, and they all retry together. The herd is still a herd. It is just a slower one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exponential backoff spreads them out
&lt;/h2&gt;

&lt;p&gt;The next step is to make each wait longer than the last: one second, then two, then four, then eight. This is exponential backoff, and it does real work. A short blip only costs your clients a second or two, while a long outage backs everyone further and further off, giving the service room to breathe.&lt;/p&gt;

&lt;p&gt;But there is still a crack in it. Clients that failed at the same moment are on the same schedule. Their retries still land in the same windows, just further apart. The herd is thinner now, but it is still moving as one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jitter scatters the herd
&lt;/h2&gt;

&lt;p&gt;The final ingredient is randomness. Instead of waiting exactly four seconds, wait a random amount of time up to four seconds. One client waits half a second, another waits three, another waits the full four. The synchronized schedule dissolves. The retries that used to arrive as one hammer blow now arrive as a drizzle the service can actually absorb.&lt;/p&gt;

&lt;p&gt;This combination has a name too: exponential backoff with jitter. The AWS Architecture Blog wrote the classic post on it ("Exponential Backoff And Jitter"), and the formula most people use is called full jitter:&lt;/p&gt;

&lt;p&gt;wait = a random number between 0 and the smaller of (cap, base times 2 to the power of attempt)&lt;/p&gt;

&lt;p&gt;The cap keeps the wait from growing forever. The base times 2 to the power of attempt is the exponential backoff. The random part is the jitter. All three matter, and most homegrown retry loops I have seen are missing at least one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;Here is a retry helper in Go that does all three. Read it top to bottom. There is nothing clever in it, which is the point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"math/rand"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// retryWithBackoff runs fn, retrying failures with exponential&lt;/span&gt;
&lt;span class="c"&gt;// backoff and full jitter. It gives up after maxAttempts.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;retryWithBackoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxAttempts&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Millisecond&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;maxWait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c"&gt;// Exponential part: 100ms, 200ms, 400ms, ...&lt;/span&gt;
        &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;maxWait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxWait&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c"&gt;// Jitter part: random between 0 and wait.&lt;/span&gt;
        &lt;span class="n"&gt;jitter&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rand&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int63n&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;After&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth noticing. The wait doubles every attempt (that &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; is a bit shift, a cheap way to multiply by powers of two). The jitter picks a random wait anywhere from zero up to that doubled value, so two clients on the same attempt number almost never sleep the same amount. And the whole thing respects the context, so a caller can cancel the retries instead of letting them run to the bitter end. Modern Go seeds its random number generator for you, so there is no seeding ceremony needed.&lt;/p&gt;

&lt;p&gt;The same idea in Python is even shorter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retry_with_backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_attempts&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="k"&gt;raise&lt;/span&gt;
            &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same three ingredients. Exponential growth, a cap, and a random pick inside the window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways people still get this wrong
&lt;/h2&gt;

&lt;p&gt;First, retrying things that should never be retried. Only retry failures that might succeed next time: timeouts, 503s, 429s. Do not retry a 400; the request itself is broken and trying again changes nothing. And only retry requests that are safe to repeat. Engineers call this idempotent: doing it twice has the same effect as doing it once. A retried "charge this card" that is not idempotent can charge it twice.&lt;/p&gt;

&lt;p&gt;Second, backoff without a cap. Left alone, base times 2 to the power of attempt grows fast. By attempt ten you are waiting almost two minutes between tries. Pick a ceiling that matches how long your caller can actually afford to wait.&lt;/p&gt;

&lt;p&gt;Third, retries with no overall deadline. A retry loop that never gives up will keep a dying request alive long past the point where anyone still wants the answer. Bound the attempts, or bound the total time, and let the caller decide what failure means.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Retries are load. Every retry policy you write is a decision about how much extra traffic your system will generate at the exact moment it is least able to handle it. Backoff and jitter are how you make that extra traffic survivable. The next time you write a retry loop, check for all three: growing waits, a cap, and randomness. If any one is missing, you are one bad twenty seconds away from finding out why.&lt;/p&gt;

&lt;p&gt;Have you ever watched a retry storm flatten a service that was already recovering? What did the graphs look like?&lt;/p&gt;

</description>
      <category>go</category>
      <category>python</category>
      <category>backend</category>
      <category>devops</category>
    </item>
    <item>
      <title>How one user's StatefulSet edit could land a pod in someone else's namespace</title>
      <dc:creator>Kashif Manzer</dc:creator>
      <pubDate>Wed, 30 Sep 2026 16:09:34 +0000</pubDate>
      <link>https://dev.to/kashif_manzer/how-one-users-statefulset-edit-could-land-a-pod-in-someone-elses-namespace-4n6a</link>
      <guid>https://dev.to/kashif_manzer/how-one-users-statefulset-edit-could-land-a-pod-in-someone-elses-namespace-4n6a</guid>
      <description>&lt;h1&gt;
  
  
  How one user's StatefulSet edit could land a pod in someone else's namespace
&lt;/h1&gt;

&lt;p&gt;On September 23, Kubernetes shipped fixes for CVE-2026-2270, a confused deputy bug in the StatefulSet controller. The short version: a user with write access to StatefulSets and ControllerRevisions in &lt;em&gt;one&lt;/em&gt; namespace could get the cluster itself to create a pod in a &lt;em&gt;different&lt;/em&gt; namespace. The cluster did the crossing. The user just pointed.&lt;/p&gt;

&lt;p&gt;If you run a shared cluster, this one is worth understanding, because the pattern behind it shows up everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wall everyone trusts
&lt;/h2&gt;

&lt;p&gt;Namespaces are the walls of Kubernetes. You give team A their own namespace, team B theirs, and RBAC rules make sure nobody reaches across. A user who can only touch &lt;code&gt;team-a&lt;/code&gt; should never be able to create anything in &lt;code&gt;team-b&lt;/code&gt;. That is the whole promise.&lt;/p&gt;

&lt;p&gt;The bug broke that promise. A namespace-scoped user, someone RBAC confined to a single namespace, could end up with a pod running in another namespace entirely. They did not hack RBAC. They did not escalate their own permissions. They asked a more powerful program to do it for them, and it agreed.&lt;/p&gt;

&lt;p&gt;That is what "confused deputy" means. The deputy is the StatefulSet controller, a cluster-wide process with the keys to every namespace. It was confused because it trusted data it should not have trusted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Suspect one: the ControllerRevision
&lt;/h2&gt;

&lt;p&gt;To follow the trail, you need to know what a ControllerRevision is. It is one of those Kubernetes objects most people never touch directly, but it is doing quiet work in the background.&lt;/p&gt;

&lt;p&gt;When you update a StatefulSet, say you change the container image from v1 to v2, the controller does not throw away the old configuration. It saves a snapshot of it in a ControllerRevision object. If the rollout goes wrong, the controller can roll back by restoring from that snapshot. Think of it as the "previous versions" feature of a document editor, but for your stateful workloads.&lt;/p&gt;

&lt;p&gt;Here is the part that matters: ControllerRevisions are regular API objects, and RBAC can grant users write access to them. In some clusters, especially multi-tenant ones, developers get broad permissions over the workload objects in their namespace, including StatefulSets and their ControllerRevisions. Nothing about that looks dangerous on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Suspect two: the cluster-wide controller
&lt;/h2&gt;

&lt;p&gt;The StatefulSet controller runs inside kube-controller-manager. It is not scoped to any namespace. It cannot be, because its job is to watch StatefulSets everywhere and reconcile them: compare what exists with what is declared, and fix the difference by creating, updating, or deleting pods.&lt;/p&gt;

&lt;p&gt;So you have a process with authority over every namespace, and it reads snapshots that ordinary users are allowed to write. If those snapshots can smuggle instructions the controller will act on, the user has effectively borrowed the controller's authority. That is exactly what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick
&lt;/h2&gt;

&lt;p&gt;When the controller restored a StatefulSet from a ControllerRevision, it restored more than it should have. The fix tells the story plainly: patched versions ensure that &lt;em&gt;only the spec field&lt;/em&gt; is restored from ControllerRevisions. Which means the old code was restoring additional fields, and those extra fields are what carried the attack across the namespace wall.&lt;/p&gt;

&lt;p&gt;An attacker with write access to both objects in their own namespace could craft a ControllerRevision containing a StatefulSet whose metadata pointed at a different namespace. The controller, doing its honest reconciliation work, would read the snapshot, restore the fields, and create the pod where the snapshot said to. The attacker would have full control over the resulting pod's metadata and specification, including the namespace selection.&lt;/p&gt;

&lt;p&gt;The user's permissions never changed. They could still only write in their own namespace. But the deputy they tricked could write anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The twist: the garbage collector was watching
&lt;/h2&gt;

&lt;p&gt;There is a catch that kept this from being a wide-open door. Kubernetes has a garbage collector that cleans up orphaned objects. A pod created in another namespace, with no valid owner there, gets deleted almost immediately.&lt;/p&gt;

&lt;p&gt;To keep the pod alive, the attacker needed a valid OwnerReference pointing at a real StatefulSet in the victim namespace. An OwnerReference is how a pod says "I belong to this StatefulSet," and the garbage collector checks it. Forging a convincing one requires the UID of an existing StatefulSet in the target namespace. That is not something you can guess from outside.&lt;/p&gt;

&lt;p&gt;So the full attack needed two things: the confused deputy trick to place the pod, and knowledge of a real StatefulSet UID in the victim namespace to keep it there. That second requirement is why the advisory notes this matters most in multi-tenant clusters, where that kind of information leaks across tenants more easily, and where broad write permissions get handed out more casually.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Kubernetes released patched versions on September 23: v1.34.12, v1.35.9, v1.36.5, and v1.37.1. The fix is surgical. The controller now restores only the &lt;code&gt;spec&lt;/code&gt; field from ControllerRevisions, cutting off the path that let attacker-controlled metadata cross the wall.&lt;/p&gt;

&lt;p&gt;The vulnerability was reported by ImanOracle, and the fix was coordinated by Maciej Szulik, Filip Křepinský, Verónica López, Jeremy Rickard, and Nathan Herz. No confirmed exploitation has been reported. The CVSS score is 5.9, medium, which reflects the high privileges required and the garbage collector catch, not the seriousness of the boundary it crossed.&lt;/p&gt;

&lt;p&gt;If you cannot upgrade immediately, the advisory's guidance is direct: treat StatefulSet and ControllerRevision write access as sensitive. That combination of permissions is more powerful than it looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson that outlives the CVE
&lt;/h2&gt;

&lt;p&gt;This bug will be patched and forgotten, but the pattern will not. Every system has deputies: CI runners that deploy with production credentials, webhooks that act on user-supplied payloads, controllers that reconcile user-written objects with cluster-wide authority. The confused deputy shows up whenever a powerful helper acts on data from a less powerful user without checking whether the user was allowed to ask for that outcome.&lt;/p&gt;

&lt;p&gt;Namespaces are still the right walls. RBAC is still the right lock. But a lock is only as good as the most trusted person holding a key, and in Kubernetes, controllers hold keys to everything. Audit who can write the objects your controllers read. In this case, that meant one RBAC review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl auth can-i create statefulsets &lt;span class="nt"&gt;--as&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;team-a-dev &lt;span class="nt"&gt;-n&lt;/span&gt; team-a
kubectl auth can-i create controllerrevisions &lt;span class="nt"&gt;--as&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;team-a-dev &lt;span class="nt"&gt;-n&lt;/span&gt; team-a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both of those return yes for users who do not need that combination, you had the same shape of problem, with or without this CVE.&lt;/p&gt;

&lt;p&gt;When was the last time you checked which roles in your cluster can write ControllerRevisions?&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>security</category>
      <category>backend</category>
    </item>
    <item>
      <title>One bad config file, 8.5 million blue screens</title>
      <dc:creator>Kashif Manzer</dc:creator>
      <pubDate>Mon, 28 Sep 2026 16:18:40 +0000</pubDate>
      <link>https://dev.to/kashif_manzer/one-bad-config-file-85-million-blue-screens-100c</link>
      <guid>https://dev.to/kashif_manzer/one-bad-config-file-85-million-blue-screens-100c</guid>
      <description>&lt;p&gt;Friday, 19 July 2024. Early morning in Europe, late night in the US.&lt;/p&gt;

&lt;p&gt;At airports, departure boards froze mid-update. In hospitals, staff reached for pen and paper. At banks and broadcasters, screens everywhere showed the same thing: a plain blue screen, a frown, and the words "Your PC ran into a problem."&lt;/p&gt;

&lt;p&gt;It was not a cyberattack. Nobody had been hacked. Something had been updated, and the update was poison.&lt;/p&gt;

&lt;h2&gt;
  
  
  The poison
&lt;/h2&gt;

&lt;p&gt;The poison came from CrowdStrike, a security company whose Falcon software runs on millions of corporate Windows machines. Falcon watches for attacks, and to keep up with new ones, CrowdStrike feeds it fresh detection rules constantly. Not once a month. Sometimes several times a day.&lt;/p&gt;

&lt;p&gt;These rapid updates are called Rapid Response Content. They travel through numbered channel files. On 19 July at 04:09 UTC, Channel File 291 went out with new rules for spotting attacks that abuse named pipes. (A named pipe is just a way for two programs on the same machine to talk to each other. Attack tools love them, so defenders watch them.)&lt;/p&gt;

&lt;p&gt;78 minutes later, at 05:27 UTC, CrowdStrike pulled the file back. But any machine that had grabbed it in that window was already doomed, and the doom had a special cruelty to it. The bad file loaded during boot. So the machine would start, load the file, crash, reboot, and start again. Forever. Fixing it meant visiting each machine by hand, booting into Safe Mode, and deleting the file.&lt;/p&gt;

&lt;p&gt;About 8.5 million Windows machines, by Microsoft's count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the poison lived
&lt;/h2&gt;

&lt;p&gt;To understand how one bad file could do this, you need to know where Falcon lives. It does not run like a normal app. Its core, a driver called CSAgent.sys, runs in kernel mode.&lt;/p&gt;

&lt;p&gt;A quick explainer, because this is the whole story. Your computer has two neighbourhoods. User mode is where apps live. If an app crashes there, the app dies and the computer carries on. Kernel mode is where the operating system core lives. If code crashes there, the whole machine goes down with it. Security software runs in kernel mode because it needs to see everything. The price is that its bugs become everyone's bugs.&lt;/p&gt;

&lt;p&gt;Now the actual defect. Months earlier, CrowdStrike had added a new detection capability: an "IPC Template Type" for watching named pipes, shipped in sensor version 7.11 on 28 February 2024. Think of a template type as a form with blanks. It declares the inputs a detection rule is allowed to use. This one declared 21 input fields.&lt;/p&gt;

&lt;p&gt;The sensor code that reads those inputs, the Content Interpreter, had been built to handle 20.&lt;/p&gt;

&lt;p&gt;On 19 July, Channel File 291 carried the first rule that actually filled in field number 21. Earlier rules had left that field on a wildcard match, which is why months of testing and four earlier deployments in March and April never tripped the bug. The moment real data landed in field 21, the interpreter read past the end of its list. In plain terms: it reached for item 21 in a list of 20.&lt;/p&gt;

&lt;p&gt;In user mode, that is one crashed program. In kernel mode, it is a blue screen of death. And because the file loads at boot, the blue screen arrived before anyone could do anything about it.&lt;/p&gt;

&lt;p&gt;A sketch of the shape of the bug (illustration only, not CrowdStrike's real code):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# the template type declares 21 fields
&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read_template_instance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# 21 items this time
&lt;/span&gt;
&lt;span class="c1"&gt;# but the interpreter was built for 20
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;field_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# i = 20 reads past the end: boom
&lt;/span&gt;    &lt;span class="nf"&gt;check_against_rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why every safety net missed it
&lt;/h2&gt;

&lt;p&gt;This is the part that should make every backend engineer uncomfortable, because none of these failures were exotic.&lt;/p&gt;

&lt;p&gt;First, the validator. CrowdStrike has a Content Validator whose entire job is to check new detection content before release. It had a logic error of its own: it never compared the number of fields in the new content against the number the interpreter could handle. The guard dog was checking IDs at the door while the fence had a hole in it.&lt;/p&gt;

&lt;p&gt;Second, the tests. The new template type had passed stress tests back in March. But the tests used wildcard matching on the 21st field, so they never walked the exact path that would crash. The tests proved the happy path worked. The bug lived on the path nobody walked.&lt;/p&gt;

&lt;p&gt;Third, the rollout. CrowdStrike deploys its sensor software in stages, rings of machines, watching each ring before widening. Sensible. But Rapid Response Content skipped all of that and went to every machine at once. The riskiest updates, the ones shipped several times a day, had the weakest rollout of anything the company shipped. The file was live for 78 minutes. That was enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Validate shapes, not just values.&lt;/strong&gt; The validator checked the content but never asked the simplest question: does this have more fields than the reader expects? Whenever two systems share a format, the count and the shape are the contract. Check the contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test the exact thing you ship.&lt;/strong&gt; A wildcard in a test is a hole in the test. If your test data cannot trigger the bug, your test is decoration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Just a config change" is a story we tell ourselves.&lt;/strong&gt; Config is code that runs with the privileges of whatever reads it. This config ran in kernel mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Roll out everything in stages, especially the stuff you ship most often.&lt;/strong&gt; Frequency is not safety. The thing you deploy ten times a day deserves the canary more than the thing you deploy twice a year, because it gets ten times as many chances to be wrong.&lt;/p&gt;

&lt;p&gt;CrowdStrike's own fix list reads like this postmortem in reverse: stricter content validation, real test cases for the 21st field, error handling in the interpreter so a bad read can never crash the machine again, and staged rollouts for Rapid Response Content.&lt;/p&gt;

&lt;p&gt;The scariest bugs are not clever. This one was a list with 21 items and a reader built for 20. The clever part was everything around it that let such a simple bug reach 8.5 million machines in 78 minutes.&lt;/p&gt;

&lt;p&gt;What is the scariest "just a config change" you have ever shipped?&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CrowdStrike's root cause analysis, via The Hacker News: &lt;a href="https://thehackernews.com/2024/08/crowdstrike-reveals-root-cause-of.html?ref=netmanageit.com" rel="noopener noreferrer"&gt;https://thehackernews.com/2024/08/crowdstrike-reveals-root-cause-of.html?ref=netmanageit.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Incident writeup: &lt;a href="https://github.com/mahdihedhli/threatpedia/blob/HEAD/site/src/content/incidents/crowdstrike-falcon-global-bsod-outage-2024.md" rel="noopener noreferrer"&gt;https://github.com/mahdihedhli/threatpedia/blob/HEAD/site/src/content/incidents/crowdstrike-falcon-global-bsod-outage-2024.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Two AI APIs die this weekend: Perplexity Sonar (Sep 27) and Appsmith AI (Sep 30)</title>
      <dc:creator>Kashif Manzer</dc:creator>
      <pubDate>Fri, 25 Sep 2026 03:05:29 +0000</pubDate>
      <link>https://dev.to/kashif_manzer/two-ai-apis-die-this-weekend-perplexity-sonar-sep-27-and-appsmith-ai-sep-30-599h</link>
      <guid>https://dev.to/kashif_manzer/two-ai-apis-die-this-weekend-perplexity-sonar-sep-27-and-appsmith-ai-sep-30-599h</guid>
      <description>&lt;h1&gt;
  
  
  Two AI APIs Die This Weekend: Perplexity Sonar (Sep 27) and Appsmith AI (Sep 30)
&lt;/h1&gt;

&lt;p&gt;Perplexity retires the Sonar API on September 27, 2026. Appsmith shuts down its managed Appsmith AI plugin on September 30. If you touch either one, this is your last week.&lt;/p&gt;

&lt;p&gt;Here is the fast version of what is happening and a 15-minute triage to find out whether you are exposed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Perplexity Sonar dies Sunday
&lt;/h2&gt;

&lt;p&gt;The announcement came in a Perplexity blog post on August 13: "On September 27, 2026, the Agent API becomes the main surface for this work, and Sonar tiers retire on the same date." Sonar calls will stop working if nothing changes.&lt;/p&gt;

&lt;p&gt;Here is the part that should make you check your vendor monitoring: the date is real but badly announced. It appears in the blog post and in the docs warning banner. It does NOT appear in the migration guide, which says Sonar "remains supported." It is not in the changelog either; the Sonar item is filed under July 2026 with no date. Perplexity's own FAQ tells you to watch the changelog for retirements. A team that follows that instruction exactly never finds this one. If your vendor monitoring diffs changelogs, this deadline is invisible to it.&lt;/p&gt;

&lt;p&gt;The replacement is the Agent API (canonical endpoint &lt;code&gt;/v1/agent&lt;/code&gt;, GA since February 2026). The preset mapping is one to one: &lt;code&gt;sonar&lt;/code&gt; to &lt;code&gt;fast&lt;/code&gt;, &lt;code&gt;sonar-pro&lt;/code&gt; to &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;sonar-reasoning-pro&lt;/code&gt; to &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;sonar-deep-research&lt;/code&gt; to &lt;code&gt;high&lt;/code&gt;, with &lt;code&gt;xhigh&lt;/code&gt; sitting above all four.&lt;/p&gt;

&lt;p&gt;Three things bite people on this migration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;It is not a model-string swap. The endpoint path, the request body, and the response parsing all change. If your UI renders source citations, that is the highest-risk silent break: the UI renders, the sources are just wrong or empty.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The cost model changes shape. Sonar billed tokens. The Agent API bills tokens plus per-tool-invocation billing: web_search $2.50, fetch_url $0.50, finance_search $5, people_search $5 per 1,000 invocations, with step budgets per preset (fast 1, low 5, medium/high 15, xhigh 100). Budget alerts calibrated on Sonar token prices will misfire.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The rate-limit trap. Tiers are cumulative lifetime spend ($0 / $50 / $250 / $500 / $1,000 / $5,000), capped at 1/3/8/17/33/33 queries per second. Tier 5 ($5,000 cumulative) buys exactly the same throughput as Tier 4 ($1,000). If your plan assumes spending more lifts the ceiling, it stops lifting at Tier 4.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Also: &lt;code&gt;sonar-pro&lt;/code&gt; has a 200K input window, the largest in the Sonar lineup. Perplexity publishes no context-window figure for the Agent API, only per-preset max output tokens. If you picked sonar-pro for the 200K path, you are migrating on an unknown. Get it in writing from Perplexity before you size the work.&lt;/p&gt;

&lt;p&gt;The Search API is not affected. It is a separate product with no deprecation notice. Customers with existing contractual commitments to Sonar are covered through the end of their current term.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appsmith AI dies three days later
&lt;/h2&gt;

&lt;p&gt;Since release 2.3, you can no longer create new Appsmith AI datasources or new queries against existing ones. The API rejects both with a clear 400 (&lt;code&gt;AE-DTS-4031&lt;/code&gt;). Existing datasources and queries keep working until September 30 and now show a non-dismissible deprecation banner with the date. Source: &lt;a href="https://github.com/appsmithorg/appsmith/pull/42015" rel="noopener noreferrer"&gt;https://github.com/appsmithorg/appsmith/pull/42015&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The migration path is BYOK: move to the OpenAI, Anthropic, or Google AI datasources with your own API keys. Managed Appsmith-owned credentials are revoked about 60 days after GA, which is the hard stop even if your instance upgrades late.&lt;/p&gt;

&lt;p&gt;Two extra traps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uploaded files (file context) will not be available after the shutdown date. There is no transfer path. Re-upload or redesign now.&lt;/li&gt;
&lt;li&gt;A recent Appsmith change made the datasource plugins live-fetch current model lists, and saved Anthropic queries on hand-typed modern models (e.g. &lt;code&gt;claude-sonnet-4-*&lt;/code&gt;) now get the raw messages-API body instead of the old completion-shaped body. If any widget or JS object binds to a &lt;code&gt;completion&lt;/code&gt; field on those queries, update the binding. Source: &lt;a href="https://github.com/appsmithorg/appsmith/pull/42114" rel="noopener noreferrer"&gt;https://github.com/appsmithorg/appsmith/pull/42114&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 15-minute triage
&lt;/h2&gt;

&lt;p&gt;Run these in order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Am I calling Sonar?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s1"&gt;'sonar(-pro|-reasoning-pro|-deep-research)?'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.py'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.js'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.ts'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.go'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.java'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.rb'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.yml'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.yaml'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.json'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.env*'&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-viE&lt;/span&gt; &lt;span class="s1"&gt;'node_modules|dist/|build/|\.git/'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also grep prompt-template stores, feature-flag configs, and IaC. A hit means Track A applies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Do I parse Sonar responses or citations?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Search for &lt;code&gt;choices[0].message.content&lt;/code&gt; and any code reading a citations array from Perplexity responses. Sonar's citation array is what most teams built source-attribution UI on; the Agent API response is a different shape. A hit means your parsing code needs rewriting, not just the model string.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Do I depend on sonar-pro's 200K context?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If any request relies on the 200K input path, you have no published equivalent to plan against. Ask Perplexity in writing before you commit a sprint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Am I on Appsmith AI?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In each Appsmith workspace, open Datasources and look for the Appsmith AI tile. If you see the non-dismissible deprecation callout on the datasource editor or any AI query editor, you are on the managed plugin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Do my Appsmith AI queries use uploaded files?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open each Appsmith AI query and check for file context. A hit means those files do not transfer; you must re-upload them against the new datasource.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; hits in 1 or 2 mean a standard Sonar migration, roughly a day. A hit in 3 means you have an open question to settle first. A hit in 4 means the Appsmith track applies; a hit in 5 adds file-context rework.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full kit
&lt;/h2&gt;

&lt;p&gt;This article is the triage. If you have hits, I built the full migration kit for both tracks: the ordered migration checklists with per-step verification, the before/after call-shape patterns, six code-review prompts you can paste into any AI assistant or hand to a reviewer, the testing and verification plan, the honest rollback story (there is no post-Sep-27 rollback for Sonar; Appsmith gets a staged retreat), and a gotchas FAQ covering the badly-announced date, the blog's overstated cost claims, the Tier 4/5 trap, and the Anthropic binding change. Every date, price, and behavior was verified against vendor docs or the deprecation PR on the build date, and anything I could not verify is marked as unverified, not hidden. It is here: &lt;a href="https://kmanzer.gumroad.com/l/dsbde" rel="noopener noreferrer"&gt;https://kmanzer.gumroad.com/l/dsbde&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sunday is three days away. If the triage shows hits, do not wait for the quarter-end freeze to make the decision for you.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>GitHub Deleted Node 20 From Actions Runners: The 15-Minute Triage Checklist</title>
      <dc:creator>Kashif Manzer</dc:creator>
      <pubDate>Thu, 24 Sep 2026 20:41:04 +0000</pubDate>
      <link>https://dev.to/kashif_manzer/github-deleted-node-20-from-actions-runners-the-15-minute-triage-checklist-4gj1</link>
      <guid>https://dev.to/kashif_manzer/github-deleted-node-20-from-actions-runners-the-15-minute-triage-checklist-4gj1</guid>
      <description>&lt;p&gt;On September 23, 2026, GitHub removed the Node.js 20 runtime from Actions runners. Any JavaScript action whose &lt;code&gt;action.yml&lt;/code&gt; still declares &lt;code&gt;runs.using: node20&lt;/code&gt; can no longer start: the job fails at action startup, before your tests even run.&lt;/p&gt;

&lt;p&gt;Timeline (from the &lt;a href="https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/" rel="noopener noreferrer"&gt;official changelog&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2025-09-19&lt;/strong&gt;: deprecation announced&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-06-16&lt;/strong&gt;: node20 actions force-run on Node 24 with a warning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-09-23&lt;/strong&gt;: Node 20 removed entirely (revised up from Sept 16)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your CI went red yesterday and you haven't touched anything: this is why. Here's how to triage it in 15 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 1: Confirm you're hit (1 min)
&lt;/h2&gt;

&lt;p&gt;Open your latest workflow run logs and search for: &lt;code&gt;Node.js 20 actions are deprecated&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Hit = your pins are stale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 2: Inventory every action pin (5 min)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rhoE&lt;/span&gt; &lt;span class="s1"&gt;'uses:[[:space:]]*[^[:space:]#]+'&lt;/span&gt; .github/workflows/&lt;span class="k"&gt;*&lt;/span&gt;.yml .github/actions/&lt;span class="k"&gt;*&lt;/span&gt;/action.yml 2&amp;gt;/dev/null | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the second path: composite actions live in &lt;code&gt;.github/actions/&lt;/code&gt; and are &lt;strong&gt;invisible to Dependabot-style tag scans&lt;/strong&gt;. You must grep them separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 3: Resolve each pin's actual runtime (5 min)
&lt;/h2&gt;

&lt;p&gt;Do not trust tag names. The node24 boundary is not uniform across majors: &lt;code&gt;upload-artifact&lt;/code&gt; only moved to node24 at v6, &lt;code&gt;download-artifact&lt;/code&gt; at v7. For each third-party pin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh api &lt;span class="s1"&gt;'repos/&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;/contents/action.yml?ref=&amp;lt;ref&amp;gt;'&lt;/span&gt; &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.content'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A1&lt;/span&gt; &lt;span class="s1"&gt;'^runs:'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;using: node20&lt;/code&gt; = broken. &lt;code&gt;using: node24&lt;/code&gt; = safe. &lt;code&gt;using: composite&lt;/code&gt; or &lt;code&gt;using: docker&lt;/code&gt; = unaffected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 4: Self-hosted runners: check the floor (2 min)
&lt;/h2&gt;

&lt;p&gt;node24 actions require runner &lt;strong&gt;&amp;gt;= 2.327.1&lt;/strong&gt;. Below that, bumped actions fail even with correct pins. Also: Node 24 doesn't run on macOS &amp;lt;= 13.4 or ARM32 at all: those jobs need a different plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bumps everyone hits
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Broken pin&lt;/th&gt;
&lt;th&gt;Minimal node24 major&lt;/th&gt;
&lt;th&gt;Watch out&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actions/checkout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;v6 moves credentials out of &lt;code&gt;.git/config&lt;/code&gt;; v7 blocks fork-PR checkout by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actions/setup-node&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;v5 enables package-manager caching &lt;strong&gt;by default&lt;/strong&gt;: disable with &lt;code&gt;package-manager-cache: false&lt;/code&gt; in privileged workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actions/setup-python&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clean bump, runtime only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actions/setup-go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;v6 honors the &lt;code&gt;toolchain&lt;/code&gt; directive and defaults &lt;code&gt;GOTOOLCHAIN=local&lt;/code&gt;: use &lt;code&gt;go-version-file: 'go.mod'&lt;/code&gt; or builds break&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actions/upload-artifact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Do NOT stop at v5: it still runs node20 by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actions/download-artifact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;v5 changed single-artifact download paths; pair with upload v6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prefer the &lt;strong&gt;minimal&lt;/strong&gt; node24 major: each extra major crossed is unrelated breaking change you absorb for nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two gotchas that bite backend repos hardest
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;setup-go@v6 silently changes which Go you build with.&lt;/strong&gt; v6 honors the &lt;code&gt;toolchain&lt;/code&gt; directive in &lt;code&gt;go.mod&lt;/code&gt; (v5 ignored it) and defaults &lt;code&gt;GOTOOLCHAIN=local&lt;/code&gt;, which forbids the silent toolchain downloads v5 allowed. If your workflow pins &lt;code&gt;go-version: 1.24&lt;/code&gt; but &lt;code&gt;go.mod&lt;/code&gt; says &lt;code&gt;go 1.25&lt;/code&gt;, builds fail. Fix: &lt;code&gt;go-version-file: 'go.mod'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no rollback to node20.&lt;/strong&gt; The runtime is gone: &lt;code&gt;FORCE_JAVASCRIPT_ACTIONS_TO_NODE24&lt;/code&gt; and friends do nothing now. "Rollback" means retreating to a &lt;em&gt;different&lt;/em&gt; node24 major, never back to the old pin.&lt;/p&gt;




&lt;p&gt;I spent today turning this into a complete one-afternoon migration kit: all 19 actions in the pin-bump map with landmines, the ordered 7-step migration, 6 code-review prompts, a testing/verification plan, the honest rollback plan, and every gotcha above sourced. It's $39 here: &lt;a href="https://kmanzer.gumroad.com/l/sreej" rel="noopener noreferrer"&gt;https://kmanzer.gumroad.com/l/sreej&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every version number in it was verified against upstream release notes or real migration reports on the build date. Good luck out there: and check your composite actions.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>github</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
