<?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: dafili</title>
    <description>The latest articles on DEV Community by dafili (@dafili).</description>
    <link>https://dev.to/dafili</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%2F4218425%2F520b8d8c-3bae-446c-9cdb-16812f9ce535.jpg</url>
      <title>DEV Community: dafili</title>
      <link>https://dev.to/dafili</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dafili"/>
    <language>en</language>
    <item>
      <title>My Backup Exited 0 for Three Weeks and Nothing Paged Me</title>
      <dc:creator>dafili</dc:creator>
      <pubDate>Sat, 31 Oct 2026 06:48:02 +0000</pubDate>
      <link>https://dev.to/dafili/my-backup-exited-0-for-three-weeks-and-nothing-paged-me-5ce6</link>
      <guid>https://dev.to/dafili/my-backup-exited-0-for-three-weeks-and-nothing-paged-me-5ce6</guid>
      <description>&lt;p&gt;A backup script on one of my boxes ran every night for three weeks. It exited 0. It wrote nothing. Nothing alerted, and from the system's perspective, nothing had failed.&lt;/p&gt;

&lt;p&gt;I found out because I needed a file from six months ago.&lt;/p&gt;

&lt;p&gt;This is not an unusual story. If you have run servers long enough, you have your own version of it. What made it interesting to me was how many different ways the same class of failure can hide, and how little of it is covered by the monitoring people typically install.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure that doesn't look like a failure
&lt;/h2&gt;

&lt;p&gt;The mental model most of us carry is: backup → error → alert. But the failures that actually bite are the ones where the exit code is zero.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The target path moved because a volume got remounted somewhere else, so &lt;code&gt;rsync&lt;/code&gt; cheerfully copied nothing.&lt;/li&gt;
&lt;li&gt;The destination filled up, and the tool wrote a zero-byte file and called it a day.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;cron&lt;/code&gt; entry was dropped during an OS upgrade. The job did not fail — it stopped existing.&lt;/li&gt;
&lt;li&gt;The script succeeded, but the snapshot retention logic deleted the new snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd&lt;/code&gt; put the unit into a failed state weeks ago and nobody looked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In none of these does the thing you are monitoring — "did the process exit non-zero" — fire.&lt;/p&gt;

&lt;p&gt;So the question has to change shape. Instead of &lt;strong&gt;"did the job succeed"&lt;/strong&gt;, ask &lt;strong&gt;"how old is the newest artifact"&lt;/strong&gt;. That question is answerable without trusting the job at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find /backups &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-printf&lt;/span&gt; &lt;span class="s1"&gt;'%T@ %p\n'&lt;/span&gt; | &lt;span class="nb"&gt;sort&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;-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the newest file is older than your RPO, you are in trouble regardless of what any exit code said. That one inversion is most of the value.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second thing I got wrong: &lt;code&gt;df -h&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;df -h&lt;/code&gt; said 40% free. The application still could not write, because &lt;strong&gt;inodes&lt;/strong&gt; were exhausted — a mail queue had accumulated a few million tiny files.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;df -h&lt;/code&gt; and &lt;code&gt;df -i&lt;/code&gt; answer different questions, and the second one fails harder when it fails:&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;df&lt;/span&gt; &lt;span class="nt"&gt;-P&lt;/span&gt;  &lt;span class="nt"&gt;-x&lt;/span&gt; tmpfs &lt;span class="nt"&gt;-x&lt;/span&gt; devtmpfs | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'NR&amp;gt;1 &amp;amp;&amp;amp; $5+0 &amp;gt; 85'&lt;/span&gt;
&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-Pi&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt; tmpfs &lt;span class="nt"&gt;-x&lt;/span&gt; devtmpfs | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'NR&amp;gt;1 &amp;amp;&amp;amp; $5+0 &amp;gt; 85'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A related trap: percent-used is the wrong metric for alerting. A volume growing at 2 GB/hour is an emergency at 60%; one sitting at 84% for two years is not. What you want is time to full:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;time_to_full ≈ (size_bytes − used_bytes) / growth_bytes_per_hour
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That requires keeping a little history, which is exactly what people skip because the dashboard already "shows disk".&lt;/p&gt;

&lt;h2&gt;
  
  
  Clock skew, the silent one
&lt;/h2&gt;

&lt;p&gt;This one surprised me. A box's clock drifted a few minutes. Nothing crashed. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TLS validation against external endpoints began failing intermittently&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cron&lt;/code&gt; fired at surprising times, because it works off wall-clock time&lt;/li&gt;
&lt;li&gt;Correlating logs across machines became fiction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;timedatectl&lt;/code&gt; will tell you whether the clock is synchronised. One line, almost nobody has the check, and when it is wrong it makes every other investigation harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;systemctl --failed&lt;/code&gt; is criminally underused
&lt;/h2&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;systemctl &lt;span class="nt"&gt;--failed&lt;/span&gt;
&lt;span class="go"&gt;  UNIT                    LOAD   ACTIVE SUB    DESCRIPTION
● backup@daily.service    loaded failed failed Daily backup
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A unit can sit in &lt;code&gt;failed&lt;/code&gt; for months. It is not in your logs unless you are looking, and if it is the backup unit, the backup has been dead the whole time while the timer rescheduled itself every night.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I ended up building
&lt;/h2&gt;

&lt;p&gt;I wanted one command that answered: is anything about to break, and did last night's backup actually run? Ten read-only checks, aggregated:&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;vaultcheck audit
&lt;span class="go"&gt;
Vaultcheck audit — web-01 — overall: CRIT

CRIT  backup    newest artifact is 22d 4h old (threshold 26h)
WARN  disk      /var at 87% (5.2 GB free)
OK    inodes    max 31% (/)
OK    memory    1.4 GB / 8.0 GB used
OK    load      0.42 / 0.31 / 0.28
CRIT  systemd   backup@daily.service failed
OK    cron      crontab present, 6 jobs
WARN  clock     not synchronised (drift ~184s)
OK    logs      log growth within limits
WARN  updates   14 pending, reboot required
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Design constraints, and why
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Standard library only.&lt;/strong&gt; No pip, no virtualenv, no C extension build step. The artifact is a single 38 KB &lt;code&gt;.pyz&lt;/code&gt; you can &lt;code&gt;scp&lt;/code&gt;. Any dependency becomes a liability when you are installing something on thirty boxes, and a monitoring tool that can itself fail to install is a bad joke.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read-only, no agent, no daemon.&lt;/strong&gt; Nothing runs between invocations. There is no state to corrupt, nothing to keep alive, nothing to fight with your init system. You can verify that claim cheaply: run &lt;code&gt;--only disk&lt;/code&gt; and watch what happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exit codes designed for automation.&lt;/strong&gt; &lt;code&gt;--fail-on warn|crit&lt;/code&gt; makes it exit non-zero at a chosen severity, so it composes with cron, systemd timers or a heartbeat service instead of asking you to look at something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No telemetry.&lt;/strong&gt; No phone-home, no account, no licence check in the audit path. A tool that inspects your servers should be inspectable itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Output formats
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vaultcheck audit &lt;span class="nt"&gt;--format&lt;/span&gt; text     &lt;span class="c"&gt;# humans&lt;/span&gt;
vaultcheck audit &lt;span class="nt"&gt;--format&lt;/span&gt; json     &lt;span class="c"&gt;# feed into anything&lt;/span&gt;
vaultcheck audit &lt;span class="nt"&gt;--format&lt;/span&gt; html &lt;span class="nt"&gt;-o&lt;/span&gt; report.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSON is the one I actually use: it lets me diff yesterday against today and only alert on change, which kills the "alert fires every night forever" failure mode that trains people to stop reading alerts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it isn't
&lt;/h2&gt;

&lt;p&gt;Worth being blunt about, because I have seen these tools oversold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; a replacement for Prometheus, Zabbix or Netdata. No time series, no graphing, no long-term storage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux only.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Closed source&lt;/strong&gt;, and &lt;strong&gt;paid&lt;/strong&gt; — one-off price, no subscription.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff I am making is "one command, no dependencies, works in ten seconds on a box you just SSH'd into" versus "fits into a mature observability stack". If you already run Prometheus with node_exporter and alert rules for backup freshness, you do not need this, and I would genuinely rather you use what you have.&lt;/p&gt;

&lt;p&gt;If you do not, the free version of 80% of it is:&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;# backup freshness (oldest acceptable: 26h)&lt;/span&gt;
find /backups &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-printf&lt;/span&gt; &lt;span class="s1"&gt;'%T@ %p\n'&lt;/span&gt; | &lt;span class="nb"&gt;sort&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;-1&lt;/span&gt;

&lt;span class="c"&gt;# disk and inodes&lt;/span&gt;
&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-P&lt;/span&gt;  &lt;span class="nt"&gt;-x&lt;/span&gt; tmpfs &lt;span class="nt"&gt;-x&lt;/span&gt; devtmpfs | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'NR&amp;gt;1 &amp;amp;&amp;amp; $5+0&amp;gt;85'&lt;/span&gt;
&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-Pi&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt; tmpfs &lt;span class="nt"&gt;-x&lt;/span&gt; devtmpfs | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'NR&amp;gt;1 &amp;amp;&amp;amp; $5+0&amp;gt;85'&lt;/span&gt;

&lt;span class="c"&gt;# failed units, cron presence, clock&lt;/span&gt;
systemctl &lt;span class="nt"&gt;--failed&lt;/span&gt;
systemctl is-enabled cron 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; systemctl is-enabled cronie 2&amp;gt;/dev/null
timedatectl | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; synchronized
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The one thing I would tell my past self
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Assert on the absence of good output, not the presence of errors.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every failure in this article — the empty backup, the full inodes, the dead cron entry, the failed unit — is invisible if your only signal is "did the process complain". Make the happy path the thing you verify, and monitor for its absence.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Two things I built around the checks above, since a few people asked in the comments:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I build a tool that does the freshness/restore checks described here: &lt;a href="https://vaultcheck.dafili.cn" rel="noopener noreferrer"&gt;vaultcheck.dafili.cn&lt;/a&gt; — the &lt;code&gt;audit&lt;/code&gt; command is essentially the four levels below, packaged so it runs in one shot and exits non-zero on failure.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Also, I extracted a list of US healthcare providers: &lt;a href="https://npilift.dafili.cn" rel="noopener noreferrer"&gt;npilift.dafili.cn&lt;/a&gt; — different project, same idea of going back to a primary source instead of trusting a reseller.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backup</category>
      <category>monitoring</category>
      <category>devops</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
