<?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: Schiff Heimlich</title>
    <description>The latest articles on DEV Community by Schiff Heimlich (@schiff_heimlich).</description>
    <link>https://dev.to/schiff_heimlich</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%2F3949704%2F89c08e96-274f-4f09-a299-8ebdabdc7096.jpg</url>
      <title>DEV Community: Schiff Heimlich</title>
      <link>https://dev.to/schiff_heimlich</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/schiff_heimlich"/>
    <language>en</language>
    <item>
      <title>Who Watches Your Watchdog? (And Why It Probably Isn't You)</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Thu, 03 Sep 2026 17:01:36 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/who-watches-your-watchdog-and-why-it-probably-isnt-you-2koh</link>
      <guid>https://dev.to/schiff_heimlich/who-watches-your-watchdog-and-why-it-probably-isnt-you-2koh</guid>
      <description>&lt;h1&gt;
  
  
  Who Watches Your Watchdog? (And Why It Probably Isn't You)
&lt;/h1&gt;

&lt;p&gt;Been thinking about this after reading about someone who built a cron-job monitoring service and had it silently stop running. The watchdog stopped watching. No alerts, no notifications. Just quiet failure.&lt;/p&gt;

&lt;p&gt;It's one of those things that sounds obvious in hindsight but catches a lot of people.&lt;/p&gt;

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

&lt;p&gt;You've got some jobs running on a schedule. Maybe they're important, maybe they're not, but you want to know when they fail. So you build or deploy something that checks on them - a watchdog service that sends you an alert if a job misses its window.&lt;/p&gt;

&lt;p&gt;This works fine. Until the watchdog itself stops working.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;p&gt;The person running PulseWatch (a Python-based cron watchdog SaaS) had it running on GitHub Actions as a scheduler. The scheduler hiccuped - not the jobs being monitored, but the thing checking the jobs. The watchdog just... stopped.&lt;/p&gt;

&lt;p&gt;Here's the thing: if your watchdog dies, you don't get notified. That's the whole point of having a watchdog, and it's also the failure mode.&lt;/p&gt;

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

&lt;p&gt;The solution was to chain the watchdog to an external probe - specifically, a Healthchecks.io endpoint. The watchdog pings Healthchecks.io on a schedule, and if Healthchecks.io doesn't hear from it, &lt;em&gt;that&lt;/em&gt; triggers an alert.&lt;/p&gt;

&lt;p&gt;Now you have two layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your watchdog monitors your jobs&lt;/li&gt;
&lt;li&gt;Healthchecks.io monitors your watchdog&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The failure domains are separated. If your watchdog dies, an external service notices.&lt;/p&gt;

&lt;h2&gt;
  
  
  The grace period trap
&lt;/h2&gt;

&lt;p&gt;There's another lesson here about how grace periods work. The author had a "15-minute cron" but assumed that meant a 15-minute grace period. It doesn't.&lt;/p&gt;

&lt;p&gt;A cron expression of &lt;code&gt;*/15 * * * *&lt;/code&gt; means "run every 15 minutes" but doesn't guarantee exactly every 15 minutes. GitHub Actions might run it at :00, :15, :30, :45 - or it might be :02, :17, :32, :47. The scheduler has its own timing.&lt;/p&gt;

&lt;p&gt;If your grace period is tied to your cron interval, you're going to get false alerts when the scheduler drifts. The fix is to make the grace period longer than the cron interval, with enough buffer for scheduling variance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check
&lt;/h2&gt;

&lt;p&gt;If you're running any kind of monitoring or alerting system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;How do you know it's still running?&lt;/strong&gt; Is there an external check on the check?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's your grace period?&lt;/strong&gt; Is it longer than your check interval plus scheduling variance?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where does the alert go?&lt;/strong&gt; If your primary alerting channel is down, is there a fallback?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The boring truth is that monitoring systems fail too. The question is whether you'll notice.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Original research from Saijo. Source: &lt;a href="https://dev.to/chriscompiles/who-watches-the-watchdog-the-boring-work-behind-a-monitoring-saas-7n8"&gt;https://dev.to/chriscompiles/who-watches-the-watchdog-the-boring-work-behind-a-monitoring-saas-7n8&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Why Your Alpine Containers Might Be Hiding a Security Blind Spot</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Sat, 29 Aug 2026 17:12:18 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/why-your-alpine-containers-might-be-hiding-a-security-blind-spot-3gbk</link>
      <guid>https://dev.to/schiff_heimlich/why-your-alpine-containers-might-be-hiding-a-security-blind-spot-3gbk</guid>
      <description>&lt;p&gt;Hey,&lt;/p&gt;

&lt;p&gt;Been looking at container base images lately and noticed something worth sharing about Alpine that might not be obvious.&lt;/p&gt;

&lt;p&gt;Most of us reach for Alpine when we want small, efficient container images. It's become pretty standard for a lot of workloads. But there's a detail about its base that's easy to overlook: BusyBox.&lt;/p&gt;

&lt;p&gt;BusyBox bundles a bunch of Unix utilities into a single executable. It's great for embedded systems, but it wasn't really built with cloud-native container security in mind. The issue isn't that BusyBox is bad—it's that when a vulnerability shows up in any part of it, you've got exposure across your entire userspace since it provides so many core utilities.&lt;/p&gt;

&lt;p&gt;What's tricky is that this isn't something your usual container scanners will flag prominently. They'll tell you about CVEs in specific packages, but the BusyBox situation is a bit different—it's more about the architecture of the base image itself.&lt;/p&gt;

&lt;p&gt;Some teams are starting to address this by looking at base images that either minimize or eliminate BusyBox from the production image build process. The idea is to move some of that security consideration earlier during the image build phase rather than relying solely on runtime scanning.&lt;/p&gt;

&lt;p&gt;If you're using Alpine (or similar images), it might be worth checking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What's actually in your base image beyond your application dependencies&lt;/li&gt;
&lt;li&gt;Whether your scanning tools are giving you visibility into the userspace components&lt;/li&gt;
&lt;li&gt;If there's a build step where you could simplify or replace parts of the base image for your specific use case&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not about abandoning Alpine—it's about understanding what's in it and making sure that fits your security model.&lt;/p&gt;

&lt;p&gt;Let me know what you've seen in your own setups.&lt;/p&gt;

&lt;p&gt;--Schiff&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>docker</category>
      <category>linux</category>
      <category>security</category>
    </item>
    <item>
      <title>When 'No space left on device' has nothing to do with disk space</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Fri, 28 Aug 2026 17:10:40 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/when-no-space-left-on-device-has-nothing-to-do-with-disk-space-10jj</link>
      <guid>https://dev.to/schiff_heimlich/when-no-space-left-on-device-has-nothing-to-do-with-disk-space-10jj</guid>
      <description>&lt;p&gt;You're running a log shipper or a config reloader and suddenly it starts failing with &lt;code&gt;ENOSPC&lt;/code&gt; — "No space left on device." You check &lt;code&gt;df -h&lt;/code&gt;, and the disk is fine. Plenty of inodes, plenty of blocks. So why is the kernel refusing to do work?&lt;/p&gt;

&lt;p&gt;Chances are it's not disk space at all. It's the inotify watch limit.&lt;/p&gt;

&lt;p&gt;inotify is the kernel's file change notification mechanism. Every directory you watch with tools like &lt;code&gt;inotifywait&lt;/code&gt;, a config reloader, or a log shipper consumes a "watch." Those watches come from a finite pool, and when you exhaust it, the kernel returns &lt;code&gt;ENOSPC&lt;/code&gt; even though the filesystem is nowhere near full. The error message is misleading, and it's easy to burn an hour chasing disk space that was never the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the limits first
&lt;/h2&gt;

&lt;p&gt;Two sysctls control this:&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="c"&gt;# Max number of watches (default is often 8192 or 65536)&lt;/span&gt;
sysctl fs.inotify.max_user_watches

&lt;span class="c"&gt;# Max number of queued events per instance&lt;/span&gt;
sysctl fs.inotify.max_user_instances
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one that bites most people is &lt;code&gt;max_user_watches&lt;/code&gt;. If you're running a bunch of containers or a monitoring agent that watches a large directory tree, 8192 goes fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirm it's actually inotify
&lt;/h2&gt;

&lt;p&gt;If you're getting &lt;code&gt;ENOSPC&lt;/code&gt; and the disk is fine, check the kernel logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dmesg | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; inotify
journalctl &lt;span class="nt"&gt;-k&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; inotify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll often see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inotify watch limit reached
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's your smoking gun. It's not disk space, it's the watch table.&lt;/p&gt;

&lt;h2&gt;
  
  
  How many watches do you actually need?
&lt;/h2&gt;

&lt;p&gt;Rough rule of thumb: one watch per directory you're monitoring. If you're watching a tree with 50,000 directories, you need at least 50,000 watches. You can get a rough count of what a tool is using with &lt;code&gt;strace&lt;/code&gt;, but for planning purposes, size it against the directory count of whatever you're shipping logs from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix it persistently
&lt;/h2&gt;

&lt;p&gt;The one-liner that works until reboot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl &lt;span class="nt"&gt;-w&lt;/span&gt; fs.inotify.max_user_watches&lt;span class="o"&gt;=&lt;/span&gt;524288
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if you want it to survive a reboot, put it in a sysctl drop-in file:&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"fs.inotify.max_user_watches=524288"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/sysctl.d/90-inotify.conf
sysctl &lt;span class="nt"&gt;--system&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sysctl --system&lt;/code&gt; applies everything in &lt;code&gt;/etc/sysctl.d/&lt;/code&gt;, so it takes effect immediately and sticks across reboots. On systemd systems this is the clean way to do it — don't edit &lt;code&gt;/etc/sysctl.conf&lt;/code&gt; directly when a drop-in file does the job and keeps your change obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part people forget
&lt;/h2&gt;

&lt;p&gt;Raising the watch limit is the easy half. The other thing to check is &lt;code&gt;max_user_instances&lt;/code&gt; — the number of separate inotify instances a single user can create. If you're spawning many short-lived watchers (some tools do this per-connection), you can hit that limit too. Bump it in the same drop-in file if you need to.&lt;/p&gt;

&lt;p&gt;Also remember: watches are a per-user resource, and the limit applies to &lt;em&gt;your&lt;/em&gt; user, not the whole box. If you run monitoring as root, root has its own budget. Container runtimes often share this, so a busy host can blow through it faster than you'd expect.&lt;/p&gt;

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

&lt;p&gt;When a monitoring tool fails with "No space left on device" and the disk is fine, don't fight the filesystem. Check &lt;code&gt;fs.inotify.max_user_watches&lt;/code&gt;, confirm with &lt;code&gt;dmesg&lt;/code&gt;, and raise it via a sysctl drop-in file. It's a ten-minute fix that looks like a disk emergency until you know what you're looking at.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>devops</category>
      <category>linux</category>
    </item>
    <item>
      <title>Nginx proxy_pass: The trailing slash that changes everything</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Thu, 27 Aug 2026 17:08:45 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/nginx-proxypass-the-trailing-slash-that-changes-everything-47b3</link>
      <guid>https://dev.to/schiff_heimlich/nginx-proxypass-the-trailing-slash-that-changes-everything-47b3</guid>
      <description>&lt;p&gt;You know how sometimes you copy an nginx config snippet and it just... doesn't work? You stare at it, reload, check the logs, and nothing seems wrong. Then you spot it: a missing or extra slash at the end of the proxy_pass line. It’s one of those tiny details that silently breaks your setup.&lt;/p&gt;

&lt;p&gt;Let’s look at two examples.&lt;/p&gt;

&lt;p&gt;First, with a trailing slash:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;location /app/ {
    proxy_pass http://backend/;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Here, nginx replaces the matched part (/app/) with the URI in proxy_pass (&lt;a href="http://backend/" rel="noopener noreferrer"&gt;http://backend/&lt;/a&gt;). So a request to /app/index.html goes to &lt;a href="http://backend/index.html" rel="noopener noreferrer"&gt;http://backend/index.html&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now, without the trailing slash:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;location /app/ {
    proxy_pass http://backend;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In this case, nginx passes the entire request URI unchanged. So /app/index.html becomes &lt;a href="http://backend/app/index.html" rel="noopener noreferrer"&gt;http://backend/app/index.html&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The difference is subtle but important. If your backend expects requests at the root (like /index.html), the first form is what you want. If it expects the path to be preserved (like /app/index.html), the second form works.&lt;/p&gt;

&lt;p&gt;I’ve seen this trip up people when they move from a location block that matches a prefix to one that’s more specific. The muscle memory of copying the proxy_pass line doesn’t account for the slash.&lt;/p&gt;

&lt;p&gt;It’s not a bug. It’s documented behavior. But it’s easy to overlook when you’re in a hurry. Next time your proxied requests are going to the wrong place, take ten seconds to check that slash.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>devops</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>That Time Your Deploy Failed Because SSH Thought You Were an Attacker</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Wed, 26 Aug 2026 17:04:51 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/that-time-your-deploy-failed-because-ssh-thought-you-were-an-attacker-4gj9</link>
      <guid>https://dev.to/schiff_heimlich/that-time-your-deploy-failed-because-ssh-thought-you-were-an-attacker-4gj9</guid>
      <description>&lt;h1&gt;
  
  
  That Time Your Deploy Failed Because SSH Thought You Were an Attacker
&lt;/h1&gt;

&lt;p&gt;You have got 30 Ansible tasks firing at once. Or maybe you are using Capistrano, or parallel-scp to push code to 30 servers at the same time. Mid-deploy, everything starts timing out. Connection refused. You blame the network. You blame AWS. You restart the deploy.&lt;/p&gt;

&lt;p&gt;It is your sshd_config.&lt;/p&gt;

&lt;p&gt;The default &lt;code&gt;MaxStartups&lt;/code&gt; setting is &lt;code&gt;10:30:60&lt;/code&gt;. Let me break that down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MaxStartups Actually Means
&lt;/h2&gt;

&lt;p&gt;The syntax is &lt;code&gt;start:rate:full&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;10&lt;/code&gt; = allow 10 unauthenticated connections before starting to drop&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;30&lt;/code&gt; = drop 30% of new connections once you hit 10&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;60&lt;/code&gt; = refuse all new connections once you hit 60&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So with 30 parallel SSH connections hitting the same sshd at once, you are going to start getting refused. Not throttled—refused. "Connection refused" errors that make you think the service is down when really sshd just does not like your burst.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Bites Deploys Specifically
&lt;/h2&gt;

&lt;p&gt;Parallel deployment tools are built for speed. They open connections to multiple hosts concurrently. If those connections all tunnel through a jump host or land on the same deployment box, you have suddenly got 30 simultaneous auth attempts from one IP.&lt;/p&gt;

&lt;p&gt;Your laptop doing 30 parallel SSH connections to prod-server-01? That is 30 connections from your IP to one port. Sshd thinks you are running a dictionary attack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MaxStartups 30:30:100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if you know your deployment patterns, just bump it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MaxStartups 50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload sshd: &lt;code&gt;sudo systemctl reload sshd&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  If You are Using a Jump Host
&lt;/h2&gt;

&lt;p&gt;The jump host takes the hit. All your parallel connections pile up there. Make sure your jump host sshd_config can handle your parallelism, or use connection multiplexing to reuse authenticated connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Debugging Clue
&lt;/h2&gt;

&lt;p&gt;Next time you see mysterious connection refused errors only during deploys, check if it correlates with the number of parallel tasks. If your network team says "nothing wrong," check your sshd_config. Probably the sshd_config.&lt;/p&gt;




&lt;p&gt;The default MaxStartups exists for good reason—it is a basic throttle against SSH brute force. But it was not designed for automated parallel workflows. Know your tool defaults before you spend an hour debugging the network.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>infrastructure</category>
      <category>linux</category>
    </item>
    <item>
      <title>That Time Client Retries Turned a Recovery Into a 7-Hour Outage</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Tue, 25 Aug 2026 17:06:20 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/that-time-client-retries-turned-a-recovery-into-a-7-hour-outage-51e2</link>
      <guid>https://dev.to/schiff_heimlich/that-time-client-retries-turned-a-recovery-into-a-7-hour-outage-51e2</guid>
      <description>&lt;p&gt;GitHub had an interesting incident last August. A component in Central US failed under load, and when it started recovering, the recovery took much longer than it should have. The culprit: clients hammering the recovering system with retries.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;During the outage, GitHub's auth system was down. When it came back up, every client that had been patiently waiting started hammering it with retry requests simultaneously. The auth system was trying to recover, but now it was drowning in retry traffic from clients that had given up waiting.&lt;/p&gt;

&lt;p&gt;This is the retry-loop trap. You've probably seen it in various forms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A health check endpoint that returns 503, so clients retry every second&lt;/li&gt;
&lt;li&gt;A connection pool that overflows, returns an error, clients reconnect immediately&lt;/li&gt;
&lt;li&gt;A CDN origin that's down, all edge nodes retry at the same rate&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The typical flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;System goes down or slow&lt;/li&gt;
&lt;li&gt;Clients start retrying with some interval&lt;/li&gt;
&lt;li&gt;System partially recovers&lt;/li&gt;
&lt;li&gt;Retries flood the recovering system&lt;/li&gt;
&lt;li&gt;System goes down again or stays slow&lt;/li&gt;
&lt;li&gt;Repeat until someone intervenes&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What actually helps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Exponential backoff with jitter&lt;/strong&gt; is the standard answer. Instead of retrying every N seconds, you retry at increasingly longer intervals with some randomness. Like: 1s, 2s, 4s, 8s, 16s... with +/- 20% jitter. This spreads out the retry load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Circuit breakers&lt;/strong&gt; stop forwarding requests to a failing service entirely for a cooldown period. The client knows the circuit is "open" and fails fast instead of retrying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting on the server side&lt;/strong&gt; helps, but during recovery you often want to gradually increase capacity, not hard-cutoff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client-side rate limiting&lt;/strong&gt; is underused. Most HTTP clients will retry indefinitely by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GitHub specific angle
&lt;/h2&gt;

&lt;p&gt;GitHub's postmortem mentioned record traffic that day - 115M Actions runs, 2.9B monthly commits. That's a lot of automated systems hammering retry loops. The lesson isn't that GitHub's infra was bad; it's that the combination of a brief outage + automated clients with naive retry logic creates a thundering herd on recovery.&lt;/p&gt;

&lt;p&gt;If you're running a service that clients depend on, it's worth thinking about what their retry behavior looks like. Are they using exponential backoff? Do they have a circuit breaker? Or are they just spinning on a 1-second loop?&lt;/p&gt;

&lt;p&gt;And if you're the client, it's worth checking your default retry config. The defaults in many HTTP libraries are not kind to recovering services.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was the GitHub Aug 17 outage lesson. Source: DevOps'ish #323 and GitHub CTO Vlad Fedorov's postmortem.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>devops</category>
      <category>sre</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>That time YAML silently broke your config because of Norway</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Mon, 24 Aug 2026 17:05:31 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/that-time-yaml-silently-broke-your-config-because-of-norway-43dj</link>
      <guid>https://dev.to/schiff_heimlich/that-time-yaml-silently-broke-your-config-because-of-norway-43dj</guid>
      <description>&lt;p&gt;A colleague pinged me last week with a config issue that had him chasing his tail for an afternoon. Turns out he was setting &lt;code&gt;country: NO&lt;/code&gt; in a YAML file, expecting it to mean Norway. YAML thought he meant boolean false.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;YAML 1.1 treats these unquoted strings as booleans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;yes&lt;/code&gt;, &lt;code&gt;no&lt;/code&gt;, &lt;code&gt;on&lt;/code&gt;, &lt;code&gt;off&lt;/code&gt;, &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Case-insensitive variants work too&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So if you have:&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;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NO&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;YAML parsers will often read &lt;code&gt;country: NO&lt;/code&gt; as &lt;code&gt;country: false&lt;/code&gt;. Your application starts, the config loads, and nobody gets an error — it just silently does the wrong thing.&lt;/p&gt;

&lt;p&gt;This has been documented for years. It bit DoorDash. It's been in Kubernetes manifests. It's in CI configs, Terraform variables, GitHub Actions, Helm charts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Matters in Practice
&lt;/h2&gt;

&lt;p&gt;You won't see a crash. Your app just reads the wrong value. The country code field becomes a boolean, silently coerces to something unexpected, and the behavior diverges from what you intended. Debugging this is annoying because there's no error — just wrong behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Helps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Quote your strings&lt;/strong&gt; — &lt;code&gt;country: "NO"&lt;/code&gt; always works. It's the safest habit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use KYAML&lt;/strong&gt; — The SIG CLI KEP 5295 introduces a strict YAML subset that quotes strings, braces maps, and brackets lists. No more ambiguity about whether whitespace or capitalization matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lint your YAML&lt;/strong&gt; — Tools like yamllint can catch some of these issues, though they won't catch semantic boolean confusion without schema awareness.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Fix in Practice
&lt;/h2&gt;

&lt;p&gt;If you're writing config that ships to others, get in the habit of quoting anything that could be misinterpreted:&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;example&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NO"&lt;/span&gt;   &lt;span class="c1"&gt;# quoted - will stay a string&lt;/span&gt;
  &lt;span class="na"&gt;flag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;     &lt;span class="c1"&gt;# boolean - this is intentional&lt;/span&gt;
  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes"&lt;/span&gt; &lt;span class="c1"&gt;# quoted - stays the string "yes", not boolean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a small discipline that prevents a class of silent failures that are otherwise hard to track down.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Source: DevOps'ish #322 (SIG CLI, KEP 5295)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>software</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>The sudoers.d Trap That'll Waste an Hour of Your Life</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Sun, 23 Aug 2026 17:03:51 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/the-sudoersd-trap-thatll-waste-an-hour-of-your-life-42ck</link>
      <guid>https://dev.to/schiff_heimlich/the-sudoersd-trap-thatll-waste-an-hour-of-your-life-42ck</guid>
      <description>&lt;h1&gt;
  
  
  The Sudoers.d Trap That'll Waste an Hour of Your Life
&lt;/h1&gt;

&lt;p&gt;If you've ever dropped a file into &lt;code&gt;/etc/sudoers.d/&lt;/code&gt; and watched your sudo rule quietly do nothing, you're not alone. This one trips up almost every Linux admin at least once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;You add a rule like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ALL &lt;span class="nv"&gt;ALL&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;ALL&lt;span class="o"&gt;)&lt;/span&gt; NOPASSWD: /usr/bin/systemctl status nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to &lt;code&gt;/etc/sudoers.d/myapp&lt;/code&gt;, hit save, and... nothing changes. The user still gets prompted for a password. Or worse — sudo breaks entirely and you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;sudo: unable to open /etc/sudoers.d/myapp: Permission denied
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Two Things That Break It Every Time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. File permissions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The file must be mode &lt;code&gt;0440&lt;/code&gt; and owned by &lt;code&gt;root:root&lt;/code&gt;. Any deviation and sudo ignores it silently.&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;chmod &lt;/span&gt;0440 /etc/sudoers.d/myapp
&lt;span class="nb"&gt;chown &lt;/span&gt;root:root /etc/sudoers.d/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Missing trailing newline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one is absurd but real. sudoers files must end with a newline. If your editor strips it or you &lt;code&gt;echo -n&lt;/code&gt; the last line, the entire file gets rejected. &lt;code&gt;visudo&lt;/code&gt; will catch this. &lt;code&gt;nano&lt;/code&gt; or &lt;code&gt;cat &amp;gt;&amp;gt;&lt;/code&gt; often won't.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Actually Debug It
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;visudo -c&lt;/code&gt; checks all sudoers files for syntax errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;visudo &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;span class="gp"&gt;/etc/sudoers.d/myapp: wrong #&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; solver specifier, line 3 of...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That output tells you exactly which file and which line. Without &lt;code&gt;-c&lt;/code&gt; you get nothing — it just silently skips the bad file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Habit
&lt;/h2&gt;

&lt;p&gt;Always edit sudoers files with &lt;code&gt;visudo -f /etc/sudoers.d/myapp&lt;/code&gt; instead of your editor of choice. It locks the file, validates syntax on save, and tells you immediately when something is wrong. It's the difference between a 5-second fix and a 45-minute incident.&lt;/p&gt;

&lt;p&gt;This isn't a deep trick. It's just the thing nobody tells you until it costs you an afternoon.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>sysadmin</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>Why Your Kubernetes Automation Keeps Getting Blocked by Humans</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Sat, 22 Aug 2026 17:03:56 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/why-your-kubernetes-automation-keeps-getting-blocked-by-humans-3og5</link>
      <guid>https://dev.to/schiff_heimlich/why-your-kubernetes-automation-keeps-getting-blocked-by-humans-3og5</guid>
      <description>&lt;p&gt;A recent survey hit on something I see all the time in cluster work: teams know they need automation, but 71% still require human review before any resource changes get applied. The 48% who said visibility and transparency would increase trust in automation? That's the actual problem.&lt;/p&gt;

&lt;p&gt;When you're asking someone to approve a change they can't verify, you're not going to get buy-in. This isn't about trust in the automation algorithm. It's about trust in the person asking them to approve it blindly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Typical Breakdown
&lt;/h2&gt;

&lt;p&gt;You've got a team running Kubernetes. Someone writes a script to right-size resource requests based on actual usage. It looks at historical CPU and memory patterns, spits out new resource specs, and... gets stuck in approval limbo because no one understands why the automation thinks a pod needs 200m CPU instead of 500m.&lt;/p&gt;

&lt;p&gt;The reviewer sees: &lt;code&gt;spec.containers[0].resources.requests.cpu: 500m → 200m&lt;/code&gt;&lt;br&gt;
The automation sees: 6 weeks of P95 usage at 180m, with headroom for burst&lt;/p&gt;

&lt;p&gt;No visibility into the reasoning = no approval.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Actually Helps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Output the analysis, not just the result&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;200m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate:&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="c1"&gt;# Analysis for app pod (namespace: production)&lt;/span&gt;
&lt;span class="c1"&gt;# Current request: 500m CPU, 512Mi memory&lt;/span&gt;
&lt;span class="c1"&gt;# &lt;/span&gt;
&lt;span class="c1"&gt;# Usage data (30 days):&lt;/span&gt;
&lt;span class="c1"&gt;#   CPU P50: 120m | P95: 175m | P99: 210m&lt;/span&gt;
&lt;span class="c1"&gt;#   Memory P50: 280Mi | P95: 420Mi | P99: 480Mi&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Recommendation: 250m CPU, 512Mi memory&lt;/span&gt;
&lt;span class="c1"&gt;# Rationale: P95 usage is 175m, recommending 250m for headroom&lt;/span&gt;
&lt;span class="c1"&gt;# Memory looks fine at current 512Mi (P99 is 480Mi)&lt;/span&gt;

&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;250m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Diff-friendly output&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Format recommendations as patches or diffs that are easy to review in Git:&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="c"&gt;# kubectl-automation-analyze namespace production --format=diff&lt;/span&gt;
- apiVersion: v1
  kind: Deployment
  metadata:
    name: app
  spec:
    template:
      spec:
        containers:
        - name: app
          resources:
            requests:
-             cpu: 500m
+             cpu: 250m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Dry-run by default&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Never apply automatically. Even if your team trusts the automation, give them the option to review. A &lt;code&gt;--auto-approve&lt;/code&gt; flag they can enable after watching it run for a week does more for adoption than any documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Takeaway
&lt;/h2&gt;

&lt;p&gt;The automation gap exists because we treat "more automation" as the goal. But if your team can't see what the automation is doing, they'll default to manual review every time. &lt;/p&gt;

&lt;p&gt;The fix isn't better algorithms. It's better output. Give people the data, show your work, and let them make an informed decision. That's not less automation - that's automation people actually use.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Bash Parameter Expansion: Two Quirks That Slip Through Code Reviews</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Fri, 21 Aug 2026 17:04:57 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/bash-parameter-expansion-two-quirks-that-slip-through-code-reviews-55ng</link>
      <guid>https://dev.to/schiff_heimlich/bash-parameter-expansion-two-quirks-that-slip-through-code-reviews-55ng</guid>
      <description>&lt;h1&gt;
  
  
  Bash Parameter Expansion: Two Quirks That Slip Through Code Reviews
&lt;/h1&gt;

&lt;p&gt;Here's a pair of bash parameter expansion behaviors that I've seen slip through code reviews and cause intermittent failures in production scripts. Small stuff, but worth knowing.&lt;/p&gt;

&lt;h2&gt;
  
  
  ${var:-default} vs ${var-default}
&lt;/h2&gt;

&lt;p&gt;This trips people up constantly. The colon matters:&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="nv"&gt;var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;  &lt;span class="c"&gt;# var is set, but empty&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;var&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# prints: default&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;var&lt;/span&gt;&lt;span class="p"&gt;-default&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;     &lt;span class="c"&gt;# prints: (empty)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the colon, bash checks if the variable is &lt;em&gt;unset OR empty&lt;/em&gt;. Without the colon, it only checks if the variable is unset. So an empty string gets treated as "use the default" with &lt;code&gt;:-&lt;/code&gt;, but as "use the empty value" without.&lt;/p&gt;

&lt;p&gt;In practice, this matters most when you're handling environment variables or config values that might legitimately be empty:&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="c"&gt;# Safe: use default only when truly absent&lt;/span&gt;
&lt;span class="nv"&gt;HOME_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;/root&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Unsafe: empty HOME would still use /root&lt;/span&gt;
&lt;span class="nv"&gt;HOME_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;-/root&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The colon form is almost always what you want when setting defaults for potentially-empty values.&lt;/p&gt;

&lt;h2&gt;
  
  
  [[ -v var ]] vs [[ -n $var ]]
&lt;/h2&gt;

&lt;p&gt;Another subtle one. &lt;code&gt;-v&lt;/code&gt; checks if a variable is &lt;em&gt;declared&lt;/em&gt; (and non-empty in bash 4.2+). &lt;code&gt;-n&lt;/code&gt; checks if the &lt;em&gt;value&lt;/em&gt; is non-empty.&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="nv"&gt;var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;  &lt;span class="c"&gt;# declared but empty&lt;/span&gt;

&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; var &lt;span class="o"&gt;]]&lt;/span&gt;    &lt;span class="c"&gt;# true (var exists)&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$var&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;   &lt;span class="c"&gt;# false ($var expands to empty string)&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$var&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="c"&gt;# false (same, quoted or not)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-n "$var"&lt;/code&gt; version is actually checking if the &lt;em&gt;quoted&lt;/em&gt; string is non-empty, which is always false for an empty variable. But &lt;code&gt;-v&lt;/code&gt; correctly identifies that the variable exists, regardless of its value.&lt;/p&gt;

&lt;p&gt;This shows up when checking if optional flags were passed:&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="nv"&gt;VERBOSE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;  &lt;span class="c"&gt;# passed but empty&lt;/span&gt;

&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; VERBOSE &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"verbose mode"&lt;/span&gt;     &lt;span class="c"&gt;# correctly matches&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$VERBOSE&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"verbose mode"&lt;/span&gt;     &lt;span class="c"&gt;# misses it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For declared/empty check: use &lt;code&gt;-v&lt;/code&gt;. For "has non-empty value" check: use &lt;code&gt;-z&lt;/code&gt; or &lt;code&gt;-n "$var"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Takeaway
&lt;/h2&gt;

&lt;p&gt;When writing scripts that handle config or environment variables, prefer &lt;code&gt;${var:-default}&lt;/code&gt; and &lt;code&gt;[[ -v var ]]&lt;/code&gt;. They're the more defensive choices and behave consistently across bash versions.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>programming</category>
      <category>software</category>
    </item>
    <item>
      <title>Nginx proxy_buffering: the one setting that quietly breaks your backend under load</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Thu, 20 Aug 2026 17:03:21 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/nginx-proxybuffering-the-one-setting-that-quietly-breaks-your-backend-under-load-1dff</link>
      <guid>https://dev.to/schiff_heimlich/nginx-proxybuffering-the-one-setting-that-quietly-breaks-your-backend-under-load-1dff</guid>
      <description>&lt;p&gt;So you have nginx proxying to a slow backend. Maybe it's a streaming endpoint, maybe it's a legacy service with occasional slow queries. Under normal load everything works fine. Then traffic spikes and suddenly you're getting 502s, timeouts, or your OOM killer fires.&lt;/p&gt;

&lt;p&gt;The culprit is often nginx's default buffering behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens
&lt;/h2&gt;

&lt;p&gt;By default, nginx buffers the &lt;em&gt;entire&lt;/em&gt; backend response before sending anything to the client. This is meant to improve performance when the backend is faster than the client. But when your backend is slow or streams data, nginx will hold the full response in memory (or disk, if it exceeds &lt;code&gt;proxy_max_temp_file_size&lt;/code&gt;) before passing it along.&lt;/p&gt;

&lt;p&gt;During this time, that nginx worker is tied up. Enough slow responses and you run out of workers, new requests queue up, and things go downhill fast.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/slow-endpoint/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://backend&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_buffering&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_buffer_size&lt;/span&gt; &lt;span class="mi"&gt;4k&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;proxy_buffering off&lt;/code&gt; tells nginx to stream the response straight through instead of buffering it. The client gets data as it arrives, workers don't get blocked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotcha with proxy_max_temp_file_size
&lt;/h2&gt;

&lt;p&gt;Even with buffering on, nginx can spill to disk. The default is 1024m. If you have many slow responses, this can fill your disk and cause the same problem. Either set it to 0 to disable disk spilling entirely, or monitor your error logs for buffer-related messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to diagnose
&lt;/h2&gt;

&lt;p&gt;If you're not sure whether buffering is your issue, check these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;strace&lt;/code&gt; on nginx workers — look for large writes to temp files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lsof&lt;/code&gt; on nginx workers — check for open temp files in the nginx tmp directory&lt;/li&gt;
&lt;li&gt;error log during load — buffer-related errors show up here&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tcpdump&lt;/code&gt; if it's localhost — see if responses come back in one chunk or streamed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When to leave buffering on
&lt;/h2&gt;

&lt;p&gt;Buffering is fine when your backend is fast and responses are small. It reduces backend load because nginx can serve cached responses without hitting the backend again. The issue only surfaces with slow or streaming backends.&lt;/p&gt;

&lt;p&gt;The fix is a single line. The diagnosing is the part that takes time if you don't know where to look.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: nginx, devops, sysadmin, performance&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>devops</category>
      <category>infrastructure</category>
      <category>performance</category>
    </item>
    <item>
      <title>When Disk Space Is Not Disk Space: The Inode Exhaustion Problem</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Wed, 19 Aug 2026 17:04:45 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/when-disk-space-is-not-disk-space-the-inode-exhaustion-problem-gm4</link>
      <guid>https://dev.to/schiff_heimlich/when-disk-space-is-not-disk-space-the-inode-exhaustion-problem-gm4</guid>
      <description>&lt;h1&gt;
  
  
  When Disk Space Is Not Disk Space: The Inode Exhaustion Problem
&lt;/h1&gt;

&lt;p&gt;You run &lt;code&gt;df -h&lt;/code&gt;, everything looks fine. Plenty of space left. Then your application throws "No space left on device" and you spend an hour debugging the wrong thing.&lt;/p&gt;

&lt;p&gt;That's inode exhaustion. Here's how it works and how to deal with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Even Is an Inode
&lt;/h2&gt;

&lt;p&gt;Every file on a filesystem has an inode. It's a metadata structure that holds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File permissions and ownership&lt;/li&gt;
&lt;li&gt;Timestamps&lt;/li&gt;
&lt;li&gt;Pointers to the actual data blocks&lt;/li&gt;
&lt;li&gt;A unique inode number&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The inode table has a fixed size when you format the filesystem. You can have millions of bytes of free space but if you run out of inodes, you can't create new files.&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="c"&gt;# Check inode usage&lt;/span&gt;
&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt;

&lt;span class="c"&gt;# Example output:&lt;/span&gt;
&lt;span class="c"&gt;# Filesystem      Inodes  IUsed  IFree IUse% Mounted on&lt;/span&gt;
&lt;span class="c"&gt;# /dev/sda1      3276800 3276800      0  100% /data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the IFree column is 0. That's your problem right there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Sneaks Up on You
&lt;/h2&gt;

&lt;p&gt;Small files are the usual culprit. Each file consumes at least one inode regardless of size. A directory with millions of tiny files (cache files, session data, log shards) can burn through your inode table fast.&lt;/p&gt;

&lt;p&gt;Common situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;/tmp filling with unnamed files from crashed processes&lt;/li&gt;
&lt;li&gt;Mail spools with tiny deferred messages&lt;/li&gt;
&lt;li&gt;Object storage caches creating countless small blobs&lt;/li&gt;
&lt;li&gt;Build artifacts that leave behind many tiny intermediate files&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Find the Culprit
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Find directories with high inode counts&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;find / &lt;span class="nt"&gt;-xdev&lt;/span&gt; &lt;span class="nt"&gt;-printf&lt;/span&gt; &lt;span class="s1"&gt;'%h\n'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;

&lt;span class="c"&gt;# Specifically look at /tmp&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;find /tmp &lt;span class="nt"&gt;-type&lt;/span&gt; f | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;

&lt;span class="c"&gt;# Check a specific directory tree&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;find /var &lt;span class="nt"&gt;-type&lt;/span&gt; f | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you find the directory, you can drill down:&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;sudo &lt;/span&gt;find /tmp &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-ls&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-k7&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The tmpfs Gotcha
&lt;/h2&gt;

&lt;p&gt;A less obvious trap: tmpfs filesystems (RAM-backed storage) mounted at /tmp or /run.&lt;/p&gt;

&lt;p&gt;tmpfs has configurable size limits:&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="c"&gt;# Check tmpfs sizes&lt;/span&gt;
mount | &lt;span class="nb"&gt;grep &lt;/span&gt;tmpfs

&lt;span class="c"&gt;# Example output:&lt;/span&gt;
&lt;span class="c"&gt;# tmpfs on /tmp type tmpfs (rw,nosuid,nodev,size=512M)&lt;/span&gt;
&lt;span class="c"&gt;# tmpfs on /run type tmpfs (rw,nosuid,nodev,size=100M)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The size= parameter limits both the byte size AND the inode count. If you set size=512M but don't set nr_inodes, the kernel auto-calculates inodes based on the size. Low memory systems often end up with surprisingly few inodes.&lt;/p&gt;

&lt;p&gt;When tmpfs fills up, you get OOM behavior even though it's supposed to be backed by RAM. The kernel starts killing processes to free memory, which is rarely what you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevention
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monitor inodes, not just space.&lt;/strong&gt; Add &lt;code&gt;df -i&lt;/code&gt; to your alerting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set explicit inode limits on tmpfs if you're using one&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   tmpfs /tmp tmpfs rw,nosuid,nodev,size=1G,nr_inodes=1M 0 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clean up strategies for temp directories.&lt;/strong&gt; Logrotate isn't enough for /tmp - processes should clean their own mess, but they don't always.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Choose filesystems wisely for small-file workloads.&lt;/strong&gt; Some filesystems (ext4 with bytes-per-inode ratio) let you tune inode density at mkfs time.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Quick Fix When You're Stuck
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# If /tmp is the problem and you need breathing room NOW&lt;/span&gt;
&lt;span class="c"&gt;# (this clears all files in /tmp - be careful)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;find /tmp &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-delete&lt;/span&gt;

&lt;span class="c"&gt;# If you need to identify files by inode number for targeted deletion&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;find /path/to/problem &lt;span class="nt"&gt;-inum&lt;/span&gt; 12345 &lt;span class="nt"&gt;-delete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real fix is figuring out why you're creating files faster than you're cleaning them up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;df -h&lt;/code&gt; is incomplete information. When something reports "no space" but &lt;code&gt;df -h&lt;/code&gt; looks fine, check &lt;code&gt;df -i&lt;/code&gt; before anything else. Inode exhaustion is rare but extremely annoying when it hits, and it always hits at the worst time.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>linux</category>
      <category>sre</category>
    </item>
  </channel>
</rss>
