<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
      <title>404human</title>
      <link>https://404human.dev</link>
      <description>Building Rust runtimes, reliable background jobs, and AI infrastructure. Sharing the experiments, failures, and releases.</description>
      <generator>Zola</generator>
      <language>en</language>
      <atom:link href="https://404human.dev/rss.xml" rel="self" type="application/rss+xml"/>
      <lastBuildDate>Wed, 14 Oct 2026 13:20:00 +0000</lastBuildDate>
      <item>
          <title>Checkpoint 1.1: I pushed a real app until it broke, and watched</title>
          <pubDate>Wed, 14 Oct 2026 13:20:00 +0000</pubDate>
          <author>404human</author>
          <link>https://404human.dev/service-monitor/checkpoint-1-1/</link>
          <guid>https://404human.dev/service-monitor/checkpoint-1-1/</guid>
          <description xml:base="https://404human.dev/service-monitor/checkpoint-1-1/">&lt;p&gt;My practice server from M0 is great for tests. It fails exactly how I tell it to, every time.&lt;/p&gt;
&lt;p&gt;But real apps don’t fail politely. They slow down a little, then a lot, then all at once. So for the first step of M1, I built a real app and pushed it until it broke, while my monitor watched.&lt;/p&gt;
&lt;h2 id=&quot;what-i-built&quot;&gt;What I built&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;A small Node.js API&lt;/strong&gt;, shaped like an AI app backend, with zero dependencies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/api/chat&lt;/code&gt; pretends to call an AI model. Most answers take about half a second, some take up to 5. It only allows 64 at once; anyone extra gets “429, too many requests”.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/search&lt;/code&gt; does real CPU work. In Node, CPU work blocks &lt;em&gt;everything else&lt;/em&gt;, including the health check.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/health&lt;/code&gt; says “I’m alive”. &lt;code&gt;/ready&lt;/code&gt; says “I can take traffic”, and honestly says &lt;em&gt;no&lt;/em&gt; when the app is stalling.&lt;/li&gt;
&lt;li&gt;A chaos switch to make it slow, broken or crashed on demand.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;A load generator&lt;/strong&gt; that ramps traffic up smoothly and reports, every second, how fast answers came back.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;An experiment script&lt;/strong&gt;, so anyone can rerun this exact test with one command.&lt;/p&gt;
&lt;h2 id=&quot;what-the-monitor-saw&quot;&gt;What the monitor saw&lt;/h2&gt;
&lt;p&gt;Traffic climbed from 20 to 400 requests a second over one minute. Meanwhile my monitor checked &lt;code&gt;/health&lt;/code&gt; and &lt;code&gt;/ready&lt;/code&gt; once a second, with a 1-second time limit:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: light-dark(#24292E, #E1E4E8); background-color: light-dark(#FFFFFF, #24292E);&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; req/s   /health               /ready                    what users got&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   ~50   0.3 ms                fine                      all good&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ~115   105 ms                fine                      still fine (p95 1.2 s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ~147   fast-ish              503 &amp;quot;event loop stalled&amp;quot;  first &amp;quot;too busy&amp;quot; replies&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ~172   no reply in 1 s       slow                      135 OK + 47 &amp;quot;too busy&amp;quot; per second&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ~197   no reply              no reply                  still mostly OK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ~305   no reply              no reply                  requests start timing out (10 s)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;what-it-taught-me&quot;&gt;What it taught me&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. Slow comes before broken.&lt;/strong&gt; The health check went from under a millisecond to over 100 ms long before anything failed. Watching that number is an early warning.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. The app warned first, then went quiet.&lt;/strong&gt; &lt;code&gt;/ready&lt;/code&gt; honestly said “not ready” before anything failed. A bit later, the app was too busy to answer &lt;em&gt;anything&lt;/em&gt;, including its own health check.&lt;/p&gt;
&lt;p&gt;An app can’t report its own trouble when it’s drowning. That’s exactly why the monitor lives &lt;em&gt;outside&lt;/em&gt; the app, with its own stopwatch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. The monitor saw it about 20 seconds before users did.&lt;/strong&gt; Users started hitting 10-second timeouts around the 45-second mark. The monitor had been failing checks since second 24.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. One check proves almost nothing.&lt;/strong&gt; In the middle of the slowdown, one check came back in 1.5 ms, between readings of 60 and 136 ms. A single sample is noisy. That’s why the next step only raises an alarm after several failures in a row.&lt;/p&gt;
&lt;h2 id=&quot;where-i-messed-up&quot;&gt;Where I messed up&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A 5-second trial run before the real experiment found two bugs.&lt;/strong&gt; My load generator used a setting before it was defined, and my script waited for the server to finish, which never happens. Lesson (again): run it small before you run it big.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;My “slow the app down” test didn’t always work.&lt;/strong&gt; 30 CPU-heavy requests stall Node badly &lt;strong&gt;if they arrive at the same moment&lt;/strong&gt;. Spread out by a few milliseconds, the exact same work causes no stall at all. Same load, completely different picture. The test now uses one long request instead, which stalls it every time.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-it-doesn-t-do-yet&quot;&gt;What it doesn’t do yet&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The monitor doesn’t check on a schedule by itself yet. A small script called it once a second for this experiment.&lt;/li&gt;
&lt;li&gt;This was one machine, one run, one minute. These numbers are real, but they aren’t a benchmark.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;up-next&quot;&gt;Up next&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Checkpoint 1.2: checking on repeat.&lt;/strong&gt; The monitor runs on its own schedule, never overlaps checks, and only raises an alarm after several failures in a row, so one noisy sample can’t cry wolf.&lt;/p&gt;
</description>
      </item>
      <item>
          <title>Checkpoint 0.7: I let a tool attack my code. It found 19 holes.</title>
          <pubDate>Wed, 14 Oct 2026 13:10:00 +0000</pubDate>
          <author>404human</author>
          <link>https://404human.dev/service-monitor/checkpoint-0-7/</link>
          <guid>https://404human.dev/service-monitor/checkpoint-0-7/</guid>
          <description xml:base="https://404human.dev/service-monitor/checkpoint-0-7/">&lt;p&gt;So far, every checkpoint ended with me breaking the code on purpose and checking that a test noticed.&lt;/p&gt;
&lt;p&gt;There’s a problem with that: &lt;strong&gt;I&lt;/strong&gt; chose what to break. I’m also the person who wrote the tests. Of course my tests caught my attacks.&lt;/p&gt;
&lt;p&gt;So for this checkpoint, I handed the attacking over to tools.&lt;/p&gt;
&lt;h2 id=&quot;attack-1-a-tool-that-plants-bugs&quot;&gt;Attack 1: a tool that plants bugs&lt;/h2&gt;
&lt;p&gt;There’s a tool called &lt;strong&gt;cargo-mutants&lt;/strong&gt;. It makes hundreds of tiny changes to your code, one at a time. It flips a &lt;code&gt;&amp;gt;&lt;/code&gt; into a &lt;code&gt;&amp;gt;=&lt;/code&gt;, deletes a line, makes a function return nothing. After each change, it runs all your tests.&lt;/p&gt;
&lt;p&gt;If the tests still pass with the bug in place, that’s a &lt;strong&gt;survivor&lt;/strong&gt;: proof that a real bug could hide there.&lt;/p&gt;
&lt;p&gt;First run, on all my code: &lt;strong&gt;238 planted bugs, 19 survived.&lt;/strong&gt; Every one of my hand-picked attacks had been caught, and the tool still found 19 holes.&lt;/p&gt;
&lt;p&gt;The survivors told a very clear story:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Edges.&lt;/strong&gt; Nothing tested a page of &lt;em&gt;exactly&lt;/em&gt; the 64 KB limit, or a web address of &lt;em&gt;exactly&lt;/em&gt; 2,048 characters. A &lt;code&gt;&amp;gt;&lt;/code&gt; quietly turning into &lt;code&gt;&amp;gt;=&lt;/code&gt; went unnoticed. Edges are where bugs live, and I’d only tested near them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Things I never actually checked.&lt;/strong&gt; A monitor set to send a &lt;code&gt;HEAD&lt;/code&gt; request could have sent &lt;code&gt;GET&lt;/code&gt; instead, and no test would have noticed. Same for the “this reply is compressed” safety check: no test ever sent a compressed reply.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two copies of the same code,&lt;/strong&gt; where only one was tested.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I wrote a test for each one, and ran the tool again. &lt;strong&gt;258 planted bugs, 0 survivors.&lt;/strong&gt; Two of them made the tests hang instead of fail (an endless loop). That still counts as caught, because the test run stops on a time limit, just more slowly.&lt;/p&gt;
&lt;h2 id=&quot;attack-2-thousands-of-random-inputs&quot;&gt;Attack 2: thousands of random inputs&lt;/h2&gt;
&lt;p&gt;My tests used examples I made up. &lt;strong&gt;Property tests&lt;/strong&gt; flip that around: I write a rule that must &lt;em&gt;always&lt;/em&gt; hold, and a tool throws thousands of random inputs at it. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Every&lt;/strong&gt; address inside every blocked range is blocked, using random addresses, not my favourites.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;::ffff:10.0.0.1&lt;/code&gt; and other “IPv4 in an IPv6 costume” forms always get the same answer as the plain address.&lt;/li&gt;
&lt;li&gt;The address checker &lt;strong&gt;never crashes&lt;/strong&gt;, no matter what string you throw at it.&lt;/li&gt;
&lt;li&gt;To catch the opposite mistake, big chunks of the real internet must always be &lt;em&gt;allowed&lt;/em&gt;. A typo that blocked half the internet would fail this.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;11 rules, about 45,000 random cases every run.&lt;/strong&gt; No counterexamples. Honestly, I was a little disappointed.&lt;/p&gt;
&lt;h2 id=&quot;attack-3-400-checks-at-once&quot;&gt;Attack 3: 400 checks at once&lt;/h2&gt;
&lt;p&gt;Then I fired 400 checks at my practice server at the same time. Each one had to get the right answer, using exactly one connection.&lt;/p&gt;
&lt;p&gt;It passed. Then I ran the whole test suite 10 times in a row, and it failed &lt;strong&gt;3 times out of 10&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Under load, a few checks reported “timed out while connecting”. Was my monitor wrong? The Linux kernel keeps a counter for exactly this situation, so I checked it before and after each run: &lt;strong&gt;124 to 420 dropped connections&lt;/strong&gt; every time.&lt;/p&gt;
&lt;p&gt;The culprit: my practice server could only keep &lt;strong&gt;128&lt;/strong&gt; connections waiting to be picked up. That’s the default in Rust’s networking, copied from the standard library. A burst of 400 overflowed it, the kernel dropped the extra connection attempts, and they took over 2 seconds to retry.&lt;/p&gt;
&lt;p&gt;So the monitor was &lt;strong&gt;right&lt;/strong&gt;. Connecting really did take too long. My test server was the weak link. I raised its waiting room to 4,096, and the result was &lt;strong&gt;0 dropped connections in 20 runs&lt;/strong&gt;, and 10 out of 10 clean runs of the full suite.&lt;/p&gt;
&lt;p&gt;Bonus: this is exactly why the plan says the monitor should never fire more than 2 checks at once at the same server. Now I’ve seen why with my own eyes.&lt;/p&gt;
&lt;h2 id=&quot;where-i-messed-up&quot;&gt;Where I messed up&lt;/h2&gt;
&lt;p&gt;The tools found two bugs &lt;strong&gt;in my tests&lt;/strong&gt;, not my code:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A test that agreed with anything.&lt;/strong&gt; To test a size limit, I used the limit’s own value from the code. So when the tool shrank the limit, my test shrank with it, and still passed. A test has to write down the expected number itself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A pipe I closed by accident.&lt;/strong&gt; One test read a line from a program’s output through a temporary helper. In Rust, a temporary is thrown away at the end of its line, and throwing it away closed the pipe. The program’s next print then crashed. It only failed sometimes, depending on timing, which is the worst kind of bug. The fix is to give the helper a name, so it lives as long as needed.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;what-it-doesn-t-mean&quot;&gt;What it doesn’t mean&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Zero survivors means the tests &lt;strong&gt;notice&lt;/strong&gt; every small change the tool knows how to make. It doesn’t mean the code is bug-free.&lt;/li&gt;
&lt;li&gt;Two planted bugs made the tests hang instead of fail (an endless loop). Those count as caught, because the test suite stops on a time limit, but they’re slower to notice.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;up-next&quot;&gt;Up next&lt;/h2&gt;
&lt;p&gt;The first milestone is done. Next, the monitor learns to &lt;strong&gt;check on repeat&lt;/strong&gt;, every minute and without overlaps. It also gets its &lt;strong&gt;first dashboard&lt;/strong&gt;, so you can watch it work in a browser, and a real &lt;strong&gt;Node.js app&lt;/strong&gt; to watch.&lt;/p&gt;
</description>
      </item>
      <item>
          <title>Checkpoint 0.6: a command you can actually run</title>
          <pubDate>Wed, 14 Oct 2026 13:00:00 +0000</pubDate>
          <author>404human</author>
          <link>https://404human.dev/service-monitor/checkpoint-0-6/</link>
          <guid>https://404human.dev/service-monitor/checkpoint-0-6/</guid>
          <description xml:base="https://404human.dev/service-monitor/checkpoint-0-6/">&lt;p&gt;Until now, the only way to use my monitor was to write a test.&lt;/p&gt;
&lt;p&gt;That’s not a product. So this checkpoint gives it a front door.&lt;/p&gt;
&lt;h2 id=&quot;what-i-built&quot;&gt;What I built&lt;/h2&gt;
&lt;p&gt;One command:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: light-dark(#24292E, #E1E4E8); background-color: light-dark(#FFFFFF, #24292E);&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;service-monitor check https://example.com/health&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It checks the address once and tells you what happened, in plain words. A few options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--expect 200,204&lt;/code&gt;: which status codes count as fine.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--timeout-ms 5000&lt;/code&gt;: how long to wait, at most.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--contains &quot;ready&quot;&lt;/code&gt;: only pass if the page says this.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--dev-allow-port 9000&lt;/code&gt;: allow exactly &lt;code&gt;127.0.0.1&lt;/code&gt; on that port, for testing on my laptop.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And it ends with an &lt;strong&gt;exit code&lt;/strong&gt;, a number that scripts read to know what happened:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt;: it’s fine.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1&lt;/code&gt;: the site is broken.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2&lt;/code&gt;: you typed something wrong.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;3&lt;/code&gt;: “I couldn’t look”, for example a blocked address.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last one matters. A script can tell “the site is down” apart from “the monitor couldn’t check”, and those need very different reactions at 3 a.m.&lt;/p&gt;
&lt;h2 id=&quot;here-it-is-for-real&quot;&gt;Here it is, for real&lt;/h2&gt;
&lt;p&gt;Against my practice server:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: light-dark(#24292E, #E1E4E8); background-color: light-dark(#FFFFFF, #24292E);&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ service-monitor check http://127.0.0.1:9124/healthy --dev-allow-port 9124&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;PASS 200 OK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  time     0.4 ms, until the headers arrived&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ service-monitor check http://127.0.0.1:9124/error --dev-allow-port 9124&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;FAIL expected {200}, got 500 Internal Server Error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ service-monitor check http://127.0.0.1:9124/hang --timeout-ms 1000 --dev-allow-port 9124&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;FAIL no reply before the deadline&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  time     1001.1 ms, until it gave up&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ service-monitor check http://169.254.169.254/latest/meta-data/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;GAP  169.254.169.254 is in 169.254.0.0/16 (link-local, incl. cloud metadata), which monitors may not reach&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The last one is the cloud’s password address from &lt;a href=&quot;https://404human.dev/service-monitor/checkpoint-0-3/&quot;&gt;Checkpoint 0.3&lt;/a&gt;. Refused before a single byte is sent.&lt;/p&gt;
&lt;p&gt;If you type something wrong, it tells you which option, in the words you typed:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: light-dark(#24292E, #E1E4E8); background-color: light-dark(#FFFFFF, #24292E);&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;service-monitor: that check can&amp;#39;t run:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  --timeout-ms: must be between 1000 and 15000 (got 20)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;does-it-actually-work&quot;&gt;Does it actually work?&lt;/h2&gt;
&lt;p&gt;6 new tests run the &lt;strong&gt;real program&lt;/strong&gt; as a separate process, against the practice server, and check its exit code and output, exactly as a script would see them. They cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pass,&lt;/li&gt;
&lt;li&gt;broken,&lt;/li&gt;
&lt;li&gt;hanging,&lt;/li&gt;
&lt;li&gt;blocked,&lt;/li&gt;
&lt;li&gt;bad input,&lt;/li&gt;
&lt;li&gt;and complete nonsense.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;73 tests&lt;/strong&gt; in the project now. &lt;strong&gt;0 warnings.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;where-i-messed-up&quot;&gt;Where I messed up&lt;/h2&gt;
&lt;p&gt;Using the command myself, instead of just testing it, found two rough edges:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The same hang got two different descriptions.&lt;/strong&gt; Sometimes it said “timed out”, sometimes “no reply before the deadline”. Two timers were racing (mine, and a backup in the web library), and whichever won picked the wording. The &lt;em&gt;answer&lt;/em&gt; was always right; only the words changed. Now every failure is described from one list of plain-English names, so the wording can’t depend on a race.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;“Connection refused” said it waited “until the headers arrived”.&lt;/strong&gt; No headers ever arrived. It now says “until it gave up”.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Tests check that the answer is right. Actually using the thing checks whether it makes sense to a human.&lt;/p&gt;
&lt;h2 id=&quot;what-it-doesn-t-do-yet&quot;&gt;What it doesn’t do yet&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;It checks once. There’s no schedule yet (that’s the next milestone).&lt;/li&gt;
&lt;li&gt;Certificate problems on secure sites still show up as “couldn’t connect” instead of their own category.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;up-next&quot;&gt;Up next&lt;/h2&gt;
&lt;p&gt;Before I call this first milestone done, I want the testing to be &lt;strong&gt;aggressive&lt;/strong&gt;. So Checkpoint 0.7 lets tools attack the code instead of me: automatic mutation testing, thousands of random inputs, and hundreds of checks at once.&lt;/p&gt;
</description>
      </item>
      <item>
          <title>Checkpoint 0.5: the first real check</title>
          <pubDate>Wed, 14 Oct 2026 12:50:00 +0000</pubDate>
          <author>404human</author>
          <link>https://404human.dev/service-monitor/checkpoint-0-5/</link>
          <guid>https://404human.dev/service-monitor/checkpoint-0-5/</guid>
          <description xml:base="https://404human.dev/service-monitor/checkpoint-0-5/">&lt;p&gt;For four checkpoints, my monitor couldn’t actually monitor anything.&lt;/p&gt;
&lt;p&gt;Now it can.&lt;/p&gt;
&lt;h2 id=&quot;what-i-built&quot;&gt;What I built&lt;/h2&gt;
&lt;p&gt;The heart of the whole project: &lt;strong&gt;one check&lt;/strong&gt;. Give it a web address and some settings, and it comes back with one of three answers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;it’s fine,&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;it’s broken,&lt;/strong&gt; and how: an error code, a timeout, a refused connection, a reply that’s too big…&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;or “I couldn’t look”.&lt;/strong&gt; That’s a &lt;em&gt;gap&lt;/em&gt;, for example when the address isn’t allowed. A gap is never counted as fine.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every check follows the same strict rules: one try, one connection, one time limit for everything. It doesn’t follow redirects, go through proxies, reuse old connections, or unzip compressed replies.&lt;/p&gt;
&lt;h2 id=&quot;how-it-works-the-short-version&quot;&gt;How it works (the short version)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One time limit covers everything,&lt;/strong&gt; from looking up the name to the very last byte. Think of a kitchen timer you start before you even pick up the phone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The address book has a bouncer.&lt;/strong&gt; When the monitor looks up a name like &lt;code&gt;example.com&lt;/code&gt;, every address that comes back must be on the public internet. If even one is private, the whole answer is refused. Mixing a real address with a private one is a known trick for sneaking past checks like this.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A broken website isn’t an error in my code. It’s the answer.&lt;/strong&gt; The check always reports what it saw, so a crash in the monitor can never be mistaken for a broken website, or the other way round.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;does-it-actually-work&quot;&gt;Does it actually work?&lt;/h2&gt;
&lt;p&gt;First, the four cases from the plan, against &lt;a href=&quot;https://404human.dev/service-monitor/checkpoint-0-4/&quot;&gt;the practice server from 0.4&lt;/a&gt;, over real connections:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fine:&lt;/strong&gt; pass (200).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Broken:&lt;/strong&gt; “500”, reported as a failure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hanging:&lt;/strong&gt; gave up when time ran out, within the 1-second limit plus a quarter-second of slack.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server off:&lt;/strong&gt; “connection refused”, in under a second.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each one used &lt;strong&gt;exactly one&lt;/strong&gt; connection.&lt;/p&gt;
&lt;p&gt;Then the security side:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An address that isn’t allowed is refused &lt;strong&gt;before anything is sent&lt;/strong&gt;. The practice server saw 0 connections.&lt;/li&gt;
&lt;li&gt;A fake address book that points a name at the cloud’s password address (&lt;code&gt;169.254.169.254&lt;/code&gt;): refused. Same for a mix of a real address and a private one.&lt;/li&gt;
&lt;li&gt;A name that can’t be looked up: reported as a DNS failure, with the reason.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;67 tests&lt;/strong&gt; in the whole project, all passing. &lt;strong&gt;0 warnings.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;where-i-messed-up&quot;&gt;Where I messed up&lt;/h2&gt;
&lt;p&gt;Two of my own mistakes, both caught in this checkpoint.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. A leaky exception.&lt;/strong&gt; Back in &lt;a href=&quot;https://404human.dev/service-monitor/checkpoint-0-3/&quot;&gt;0.3&lt;/a&gt;, I let &lt;em&gt;names&lt;/em&gt; use the special “testing on my laptop” port, assuming the address check would keep them honest. But the address check never sees the port number, which gets added later. So in testing mode, a name could have reached &lt;em&gt;any&lt;/em&gt; port on my laptop.&lt;/p&gt;
&lt;p&gt;Fixed: only the literal address &lt;code&gt;127.0.0.1&lt;/code&gt; gets the exception now. Names always follow the public rules.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. A test that couldn’t fail.&lt;/strong&gt; I broke the “never reuse a connection” setting on purpose, and every test still passed.&lt;/p&gt;
&lt;p&gt;Why? My practice server always says “close the connection after this”, so reuse could never happen in a test, and the setting was invisible. I added a practice address that keeps connections open, plus a test that runs two checks and expects two connections. Now, breaking the setting fails the test: 1 connection instead of 2.&lt;/p&gt;
&lt;p&gt;That’s the whole point of breaking things on purpose. A passing test only means something if it &lt;em&gt;can&lt;/em&gt; fail.&lt;/p&gt;
&lt;p&gt;(Plus a small Rust one: the compiler refused a line that walks through a chain of errors. One extra character, &lt;code&gt;&amp;amp;&lt;/code&gt;, fixed it, and it’s now in my Rust notes so it doesn’t happen twice.)&lt;/p&gt;
&lt;h2 id=&quot;what-it-doesn-t-do-yet&quot;&gt;What it doesn’t do yet&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Problems with a site’s security certificate show up as “couldn’t connect”, instead of getting their own category. That needs its own test server, in M1.&lt;/li&gt;
&lt;li&gt;With a time limit under 2 seconds, a slow start to the connection is reported as “no reply in time” rather than “slow to connect”.&lt;/li&gt;
&lt;li&gt;It does one check, once. There’s no schedule yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;up-next&quot;&gt;Up next&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Checkpoint 0.6: a command I can actually run.&lt;/strong&gt; &lt;code&gt;service-monitor check &amp;lt;url&amp;gt;&lt;/code&gt; will print exactly what the check saw. Then the first milestone is done.&lt;/p&gt;
</description>
      </item>
      <item>
          <title>Checkpoint 0.4: a test server that fails on purpose</title>
          <pubDate>Wed, 14 Oct 2026 12:40:00 +0000</pubDate>
          <author>404human</author>
          <link>https://404human.dev/service-monitor/checkpoint-0-4/</link>
          <guid>https://404human.dev/service-monitor/checkpoint-0-4/</guid>
          <description xml:base="https://404human.dev/service-monitor/checkpoint-0-4/">&lt;p&gt;You can’t test a smoke alarm without smoke.&lt;/p&gt;
&lt;p&gt;To test a monitor, I need something to break: on demand, in exactly the same way, every time.&lt;/p&gt;
&lt;p&gt;Fake responses won’t do. The nastiest monitoring bugs are about timing: a server that picks up the phone and then says nothing, or one that promises a long answer and stops after a few words. You only see those over a real connection.&lt;/p&gt;
&lt;h2 id=&quot;what-i-built&quot;&gt;What I built&lt;/h2&gt;
&lt;p&gt;A small web server whose only job is to misbehave. Each address is a different kind of bad day:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/healthy&lt;/code&gt;: everything’s fine.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/error&lt;/code&gt;: “500, something broke”.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/status/404&lt;/code&gt; (or any other code): a valid answer, just not the one you expected.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/hang&lt;/code&gt;: listens to your request, then never answers. Ever.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/slow-headers/3000&lt;/code&gt;: answers, eventually (3 seconds later).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/stall-body&lt;/code&gt;: promises 1,000 bytes, sends 16, then goes quiet.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/big-body&lt;/code&gt;: 1 MB, sixteen times more than the monitor will ever read.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/close-early&lt;/code&gt;: hangs up without a word.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There’s also an address where nobody’s listening (a server that’s down), a counter of how many connections came in, and a small app to play with it by hand.&lt;/p&gt;
&lt;p&gt;It lives in its own separate project, right next to the monitor. That way, test code can never sneak into the real monitor.&lt;/p&gt;
&lt;h2 id=&quot;how-it-works-the-short-version&quot;&gt;How it works (the short version)&lt;/h2&gt;
&lt;p&gt;It speaks just enough HTTP, written by hand. A web framework would helpfully tidy up and time out for me, which is exactly what I don’t want. Here, &lt;code&gt;/hang&lt;/code&gt; really hangs.&lt;/p&gt;
&lt;p&gt;The interesting part is switching it off. A test that leaves a hanging connection behind could mess up the next test.&lt;/p&gt;
&lt;p&gt;In Rust, every value has one owner, and when the owner is done, the value gets cleaned up. So I set it up like a chain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the test owns the practice server,&lt;/li&gt;
&lt;li&gt;the server owns its background worker,&lt;/li&gt;
&lt;li&gt;the worker owns a container holding every open connection.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When a test finishes, the server gets thrown away. That stops the worker, which empties the container, which closes every connection. One thing goes out of scope, and everything closes. No “please shut down” messages, no bookkeeping.&lt;/p&gt;
&lt;p&gt;(I checked the library’s source code to be sure: throwing away that container really does stop everything inside it.)&lt;/p&gt;
&lt;h2 id=&quot;does-it-actually-work&quot;&gt;Does it actually work?&lt;/h2&gt;
&lt;p&gt;12 tests talk to it over a real network connection and check both &lt;em&gt;what&lt;/em&gt; comes back and &lt;em&gt;when&lt;/em&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/hang&lt;/code&gt; sends nothing for 300 ms, and keeps the line open.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/stall-body&lt;/code&gt; promises 1,000 bytes and sends exactly 16.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/big-body&lt;/code&gt; delivers exactly 1,048,576 bytes.&lt;/li&gt;
&lt;li&gt;Throwing the server away ends a connection that was stuck on &lt;code&gt;/hang&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And here it is by hand, with &lt;code&gt;curl&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: light-dark(#24292E, #E1E4E8); background-color: light-dark(#FFFFFF, #24292E);&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;/healthy       → 200&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;/error         → 500&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;/status/404    → 404&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;/close-early   → empty reply (curl exit 52)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;/hang          → still nothing after 1 s (curl exit 28)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;/big-body      → 1048576 bytes&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I broke it on purpose, three times:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Made &lt;code&gt;/hang&lt;/code&gt; answer:&lt;/strong&gt; 2 tests failed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Made &lt;code&gt;/stall-body&lt;/code&gt; honest:&lt;/strong&gt; 1 test failed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Let connections run on their own, outside the container:&lt;/strong&gt; the “throwing it away closes everything” test failed. That’s the proof that the ownership chain is doing the work.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;where-i-messed-up&quot;&gt;Where I messed up&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;My first “break it on purpose” attempt didn’t even compile.&lt;/strong&gt; A broken version that doesn’t compile proves nothing about the tests. So I fixed my broken version until it compiled, and then the test caught it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I’d told the code reviewer that &lt;code&gt;unwrap()&lt;/code&gt; is fine in tests.&lt;/strong&gt; Turns out that only covers the test functions themselves, not the little helper functions next to them. Nine warnings. One clearly commented line fixed it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-it-doesn-t-do-yet&quot;&gt;What it doesn’t do yet&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;It answers every kind of request the same way. There’s no special handling for HEAD requests.&lt;/li&gt;
&lt;li&gt;“An address where nobody’s listening” is found by grabbing a free port and letting it go. Another program could grab it in between, which is unlikely on my laptop.&lt;/li&gt;
&lt;li&gt;The “nothing happens” tests wait 300 ms. On a very busy machine that could get flaky. I’ll keep an eye on it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;up-next&quot;&gt;Up next&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Checkpoint 0.5: the monitor’s actual check.&lt;/strong&gt; One request against this practice server, for the four basic cases: fine, broken, hanging, and down. Each one must be judged correctly within its time limit, using exactly one connection.&lt;/p&gt;
</description>
      </item>
      <item>
          <title>Checkpoint 0.3: 127.0.0.1 has a lot of disguises</title>
          <pubDate>Wed, 14 Oct 2026 12:30:00 +0000</pubDate>
          <author>404human</author>
          <link>https://404human.dev/service-monitor/checkpoint-0-3/</link>
          <guid>https://404human.dev/service-monitor/checkpoint-0-3/</guid>
          <description xml:base="https://404human.dev/service-monitor/checkpoint-0-3/">&lt;p&gt;Every one of these means “this computer”:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;http://127.0.0.1/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://2130706433/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://0x7f.1/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://127.1/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://0177.0.0.1/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://[::ffff:127.0.0.1]/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://[64:ff9b::7f00:1]/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A monitor visits whatever address you give it. That makes it a bit like a delivery driver who’ll go to any address on the label. Handy, until someone writes the address of your private office on it.&lt;/p&gt;
&lt;p&gt;The classic target is &lt;code&gt;169.254.169.254&lt;/code&gt;, a special address inside cloud servers that hands out passwords to anything asking from the inside. This trick even has a name: SSRF.&lt;/p&gt;
&lt;p&gt;This checkpoint is the first lock on that door.&lt;/p&gt;
&lt;h2 id=&quot;what-i-built&quot;&gt;What I built&lt;/h2&gt;
&lt;p&gt;Two checks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Is the address safe to write down?&lt;/strong&gt; Only &lt;code&gt;http&lt;/code&gt; and &lt;code&gt;https&lt;/code&gt;. No username or password in it. Nothing that looks like a secret, such as &lt;code&gt;?token=&lt;/code&gt;. At most 2,048 characters. Secrets belong in a separate header, not in an address that ends up in logs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Is it safe to go there?&lt;/strong&gt; Only addresses on the public internet, on the normal web ports (80 and 443).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Good news: the library that reads web addresses turns all those disguises into plain &lt;code&gt;127.0.0.1&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; my check even looks. One address to judge, not seven.&lt;/p&gt;
&lt;h2 id=&quot;not-from-memory&quot;&gt;Not from memory&lt;/h2&gt;
&lt;p&gt;“Which addresses are private?” sounds like common knowledge. It’s a surprisingly long list.&lt;/p&gt;
&lt;p&gt;So I took it straight from the official registry (IANA) the same day: 26 special IPv4 ranges and 25 IPv6 ranges, each marked whether it’s reachable from the public internet. A test checks every line of my list against the registry’s own text, so a typo breaks the build.&lt;/p&gt;
&lt;h2 id=&quot;the-sneaky-ipv6-ones&quot;&gt;The sneaky IPv6 ones&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;For IPv6, I flipped it around.&lt;/strong&gt; Instead of listing what’s banned, only the normal public range is allowed. Anything new or unusual is refused by default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;64:ff9b::7f00:1&lt;/code&gt; is &lt;code&gt;127.0.0.1&lt;/code&gt; wearing an IPv6 costume.&lt;/strong&gt; Some networks use these addresses legitimately to reach older ones, so I don’t ban them all. I look at the address inside and judge that.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rust’s own check says &lt;code&gt;::ffff:127.0.0.1&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; “this computer”&lt;/strong&gt; until you convert it. So I convert first, every time.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-one-exception&quot;&gt;The one exception&lt;/h2&gt;
&lt;p&gt;For testing on my laptop, there’s a developer mode that allows &lt;strong&gt;exactly&lt;/strong&gt; &lt;code&gt;127.0.0.1&lt;/code&gt; on one chosen port. Not &lt;code&gt;::1&lt;/code&gt;, not &lt;code&gt;127.0.0.2&lt;/code&gt;. And only the command line can switch it on, never the web API.&lt;/p&gt;
&lt;h2 id=&quot;does-it-actually-work&quot;&gt;Does it actually work?&lt;/h2&gt;
&lt;p&gt;I broke the three defences an attacker would go after, one at a time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Skipped the check for addresses written as numbers:&lt;/strong&gt; 3 tests failed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Removed the step that converts the IPv6 costume:&lt;/strong&gt; 1 test failed. The interesting part: the address was &lt;em&gt;still blocked&lt;/em&gt;, by a backup rule. Only the test that checks the exact reason noticed. Two layers, and the tests can see both.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shrank the cloud-password range:&lt;/strong&gt; 5 tests failed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then I put the original code back and compared it byte for byte.&lt;/p&gt;
&lt;h2 id=&quot;where-i-messed-up-i-broke-this-website&quot;&gt;Where I messed up: I broke this website&lt;/h2&gt;
&lt;p&gt;When I first published this build log, every deploy said “success”, and every post on this site turned into “404 not found”.&lt;/p&gt;
&lt;p&gt;The old hosting ran two competing deploys on every update, and the wrong one happened to finish last.&lt;/p&gt;
&lt;p&gt;Lesson: a green checkmark isn’t a working site. Open the page.&lt;/p&gt;
&lt;p&gt;The fix: this site now lives on Cloudflare Pages. I build it on my own machine and upload exactly that, so there’s one deploy, and it’s the one I checked.&lt;/p&gt;
&lt;h2 id=&quot;what-it-doesn-t-do-yet&quot;&gt;What it doesn’t do yet&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;This is the first layer. Names like &lt;code&gt;example.com&lt;/code&gt; get turned into addresses later, and every one of those addresses gets checked too (Checkpoint 0.5). A name that points to a mix of public and private addresses will be refused outright, because that’s a known trick.&lt;/li&gt;
&lt;li&gt;An app checking addresses isn’t a firewall. The plan adds network rules as a second layer later.&lt;/li&gt;
&lt;/ul&gt;
</description>
      </item>
      <item>
          <title>Checkpoint 0.2: making bad settings impossible</title>
          <pubDate>Wed, 14 Oct 2026 12:20:00 +0000</pubDate>
          <author>404human</author>
          <link>https://404human.dev/service-monitor/checkpoint-0-2/</link>
          <guid>https://404human.dev/service-monitor/checkpoint-0-2/</guid>
          <description xml:base="https://404human.dev/service-monitor/checkpoint-0-2/">&lt;p&gt;Most settings bugs come from the same place: a value gets checked in one spot, then copied, changed or trusted somewhere else.&lt;/p&gt;
&lt;p&gt;Think of a bouncer at a club. Checking IDs at every table inside is slow, and someone always gets missed. Check once at the door, and everyone inside is fine.&lt;/p&gt;
&lt;h2 id=&quot;what-i-built&quot;&gt;What I built&lt;/h2&gt;
&lt;p&gt;A front door for the monitor’s settings.&lt;/p&gt;
&lt;p&gt;Settings arrive as &lt;strong&gt;raw&lt;/strong&gt; settings: whatever you typed, trusted by nobody. There’s exactly one way to turn them into &lt;strong&gt;validated&lt;/strong&gt; settings, and that’s passing every rule:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;check every 30 seconds to 1 hour,&lt;/li&gt;
&lt;li&gt;give up on a slow site after 1 to 15 seconds,&lt;/li&gt;
&lt;li&gt;a name of 1 to 80 characters,&lt;/li&gt;
&lt;li&gt;and a few more.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once settings are through the door:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;They can’t change.&lt;/strong&gt; Editing creates a new version instead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;They can’t be copied by accident.&lt;/strong&gt; They’ll be shared, not duplicated.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And the door reports &lt;strong&gt;every&lt;/strong&gt; problem at once, like a form that highlights all the wrong fields instead of making you fix them one at a time.&lt;/p&gt;
&lt;h2 id=&quot;how-rust-helps-the-short-version&quot;&gt;How Rust helps (the short version)&lt;/h2&gt;
&lt;p&gt;In Rust, every value has exactly one owner. When the raw settings walk through the door, their text &lt;em&gt;moves&lt;/em&gt; into the validated version instead of being copied. Nothing is duplicated, and nothing is left behind to go stale.&lt;/p&gt;
&lt;p&gt;The whole door is one line wherever settings come in:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: light-dark(#24292E, #E1E4E8); background-color: light-dark(#FFFFFF, #24292E);&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt;let&lt;/span&gt;&lt;span&gt; config&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #B392F0);&quot;&gt; ValidatedConfig&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt;::&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #B392F0);&quot;&gt;try_from&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;raw&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt;?&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That reads as: “try to turn the raw settings into validated ones, and stop here if any rule fails.”&lt;/p&gt;
&lt;h2 id=&quot;small-things-measured&quot;&gt;Small things, measured&lt;/h2&gt;
&lt;p&gt;I like knowing exactly how much memory things take, so these sizes are checked by tests. If a later change makes them bigger, a test fails:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;8 bytes&lt;/strong&gt; for an “ID, or nothing”. Normally it would be 16. But ID numbers start at 1, so Rust uses 0 to mean “nothing” for free.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;16 bytes&lt;/strong&gt; of bookkeeping for a name, instead of the usual 24. Text that never grows doesn’t need room to grow.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;56 bytes&lt;/strong&gt; for the list of “good” HTTP status codes, and checking a code is a single step.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also, limits count &lt;strong&gt;characters, not bytes&lt;/strong&gt;. “é” counts as one character, even though it takes two bytes.&lt;/p&gt;
&lt;h2 id=&quot;a-rule-that-can-never-fire&quot;&gt;A rule that can never fire&lt;/h2&gt;
&lt;p&gt;The plan says the timeout must be at most half the check interval. But with the plan’s own limits (interval at least 30 s, timeout at most 15 s), that’s always true.&lt;/p&gt;
&lt;p&gt;I kept the check anyway and tested it directly. If someone changes a limit later, it won’t quietly break.&lt;/p&gt;
&lt;h2 id=&quot;does-it-actually-work&quot;&gt;Does it actually work?&lt;/h2&gt;
&lt;p&gt;All 20 tests passed on the first run. That proves less than it sounds: a test that would also pass on broken code is useless.&lt;/p&gt;
&lt;p&gt;So I broke two rules on purpose:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Turned off “a HEAD request can’t check the page’s content”: &lt;strong&gt;1 test failed&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Turned off “no hidden control characters in names”: &lt;strong&gt;2 tests failed&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then I put the code back exactly as it was. Good: the tests notice.&lt;/p&gt;
&lt;h2 id=&quot;where-i-messed-up&quot;&gt;Where I messed up&lt;/h2&gt;
&lt;p&gt;I checked the code’s formatting with a one-line command that printed &lt;strong&gt;“clean”&lt;/strong&gt;, while the formatter was complaining right above it.&lt;/p&gt;
&lt;p&gt;The reason: when you chain commands with a pipe (&lt;code&gt;|&lt;/code&gt;), the shell reports how the &lt;em&gt;last&lt;/em&gt; command went. The last one was perfectly happy.&lt;/p&gt;
&lt;p&gt;Lesson: run checks on their own, especially the ones you trust.&lt;/p&gt;
&lt;h2 id=&quot;what-it-doesn-t-do-yet&quot;&gt;What it doesn’t do yet&lt;/h2&gt;
&lt;p&gt;The web address is only checked for length so far. Making sure it can’t point at your private network comes next.&lt;/p&gt;
</description>
      </item>
      <item>
          <title>Checkpoint 0.1: rules first, code second</title>
          <pubDate>Wed, 14 Oct 2026 12:10:00 +0000</pubDate>
          <author>404human</author>
          <link>https://404human.dev/service-monitor/checkpoint-0-1/</link>
          <guid>https://404human.dev/service-monitor/checkpoint-0-1/</guid>
          <description xml:base="https://404human.dev/service-monitor/checkpoint-0-1/">&lt;p&gt;Nobody gets excited about an empty project.&lt;/p&gt;
&lt;p&gt;But it’s the cheapest moment to decide which mistakes you’ll never allow, because there’s no old code to clean up yet.&lt;/p&gt;
&lt;h2 id=&quot;how-i-m-building-this&quot;&gt;How I’m building this&lt;/h2&gt;
&lt;p&gt;Every milestone is cut into small &lt;strong&gt;checkpoints&lt;/strong&gt;. A checkpoint only counts when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;its tests pass,&lt;/li&gt;
&lt;li&gt;Rust’s strict code reviewer (a tool called clippy) has &lt;strong&gt;zero&lt;/strong&gt; complaints,&lt;/li&gt;
&lt;li&gt;it’s saved in git,&lt;/li&gt;
&lt;li&gt;and it’s written up here, mistakes included.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-i-built&quot;&gt;What I built&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;An empty Rust project, split into nine parts:&lt;/strong&gt; checking, scheduling, storage, alerts and so on. Each part has one sentence saying what it does, and what it must never touch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two libraries, pinned to exact versions:&lt;/strong&gt; tokio, the engine that lets a program do many things at once, and reqwest, for making web requests.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-trick-leaving-a-feature-out&quot;&gt;The trick: leaving a feature out&lt;/h2&gt;
&lt;p&gt;On &lt;a href=&quot;https://404human.dev/service-monitor/day-0-read-the-source-first/&quot;&gt;day 0&lt;/a&gt; I found that the web library retries some failed requests by itself. There’s a setting to turn that off, and I’ll use it.&lt;/p&gt;
&lt;p&gt;But settings get forgotten. So I also left out the part of the library where those retries live (HTTP/2, which a simple health check doesn’t need).&lt;/p&gt;
&lt;p&gt;I checked the build afterwards: that code isn’t in there at all. It can’t misbehave if it doesn’t exist.&lt;/p&gt;
&lt;h2 id=&quot;rules-the-compiler-now-enforces&quot;&gt;Rules the compiler now enforces&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No &lt;code&gt;unsafe&lt;/code&gt; code.&lt;/strong&gt; Rust’s safety checks stay switched on everywhere in my code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No &lt;code&gt;unwrap()&lt;/code&gt; outside tests.&lt;/strong&gt; That’s Rust’s “assume this worked, and crash if it didn’t”. A monitor should report a problem, not fall over.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A name-clash check.&lt;/strong&gt; From &lt;a href=&quot;https://404human.dev/posts/i-made-an-ai-read-the-rust-book/&quot;&gt;my AI Rust exams&lt;/a&gt;: give a helper the same name as a built-in method, and Rust can quietly call the wrong one.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And one more choice: if a single check crashes, the monitor writes it down as a gap instead of taking the whole thing down with it.&lt;/p&gt;
&lt;h2 id=&quot;numbers&quot;&gt;Numbers&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;16 seconds&lt;/strong&gt; to build the web library and its encryption code, using just the standard compiler on this laptop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0&lt;/strong&gt; warnings. &lt;strong&gt;0&lt;/strong&gt; tests, because there’s nothing to test yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-it-doesn-t-do-yet&quot;&gt;What it doesn’t do yet&lt;/h2&gt;
&lt;p&gt;Nothing runs. This checkpoint is the foundation, and nothing more.&lt;/p&gt;
</description>
      </item>
      <item>
          <title>I read the code before writing the code. It found 4 traps.</title>
          <pubDate>Wed, 14 Oct 2026 12:00:00 +0000</pubDate>
          <author>404human</author>
          <link>https://404human.dev/service-monitor/day-0-read-the-source-first/</link>
          <guid>https://404human.dev/service-monitor/day-0-read-the-source-first/</guid>
          <description xml:base="https://404human.dev/service-monitor/day-0-read-the-source-first/">&lt;p&gt;Have you ever had a website go down and found out from an angry customer instead of your own alerts?&lt;/p&gt;
&lt;p&gt;That’s the job of an uptime monitor. It checks your site every minute and tells you when something breaks. Simple idea.&lt;/p&gt;
&lt;p&gt;But a monitor has one job: telling the truth. And the dangerous bugs in a monitor aren’t the ones that crash it. They’re the quiet ones: a check that secretly retries until it passes, or a gap in the data that shows up as green.&lt;/p&gt;
&lt;p&gt;I’m building one anyway. It’s called &lt;strong&gt;Service Monitor&lt;/strong&gt;, it’s written in Rust, and I’m building it with Claude Code, in the open. This is the first entry in &lt;a href=&quot;https://404human.dev/service-monitor/&quot;&gt;the build log&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-rule-don-t-trust-memory&quot;&gt;The rule: don’t trust memory&lt;/h2&gt;
&lt;p&gt;Programmers (and AIs) often write code from memory of how a library works. But libraries change, and memory is optimistic.&lt;/p&gt;
&lt;p&gt;So before writing any monitor code, Claude read the actual source code of the exact library versions we’d use, and ran tiny experiments to confirm what it found.&lt;/p&gt;
&lt;p&gt;Four things surprised us.&lt;/p&gt;
&lt;h2 id=&quot;trap-1-an-ip-address-skips-the-safety-check&quot;&gt;Trap 1: an IP address skips the safety check&lt;/h2&gt;
&lt;p&gt;A monitor visits whatever web address you give it. That’s handy, and also a little risky. Someone could point it at things inside your private network, like the special address on cloud servers that hands out passwords to anything that asks from inside. (Security folks call this SSRF.)&lt;/p&gt;
&lt;p&gt;The usual guard checks every address when a name like &lt;code&gt;example.com&lt;/code&gt; gets looked up. But when the address is already a number, like &lt;code&gt;http://[::ffff:127.0.0.1]/&lt;/code&gt;, there’s nothing to look up, so the guard never runs.&lt;/p&gt;
&lt;p&gt;In our test, that address reached a private test server, and the guard was called &lt;strong&gt;0 times&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; check addresses written as numbers directly, before anything goes out.&lt;/p&gt;
&lt;h2 id=&quot;trap-2-hidden-retries&quot;&gt;Trap 2: hidden retries&lt;/h2&gt;
&lt;p&gt;My rule is “one check, one try”. If a service is flaky, I want to see it. If it’s struggling, I don’t want to pile on.&lt;/p&gt;
&lt;p&gt;It turns out the web library quietly retries some requests: up to &lt;strong&gt;2 extra attempts&lt;/strong&gt; for certain errors. And a layer underneath it retries too, with no setting to turn it off.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; switch off the first, avoid the conditions that trigger the second, and leave out the feature they live in.&lt;/p&gt;
&lt;h2 id=&quot;trap-3-the-clock-that-sleeps-with-your-laptop&quot;&gt;Trap 3: the clock that sleeps with your laptop&lt;/h2&gt;
&lt;p&gt;Rust’s usual stopwatch stops counting while a laptop is asleep. On my machine it said &lt;strong&gt;17,240 seconds&lt;/strong&gt; since startup, while the clock that also counts sleep said &lt;strong&gt;62,115&lt;/strong&gt;. That’s about 12.5 hours of sleep it never noticed.&lt;/p&gt;
&lt;p&gt;For a monitor, that means checks missed while the laptop slept would simply vanish, instead of showing up as “we weren’t watching”.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; work out every check time from the real clock, and record missed checks as gaps.&lt;/p&gt;
&lt;h2 id=&quot;trap-4-the-database-that-doesn-t-mention-typos&quot;&gt;Trap 4: the database that doesn’t mention typos&lt;/h2&gt;
&lt;p&gt;SQLite silently ignores a setting it doesn’t recognise. Mistype the one that makes writes crash-safe, and you get no error, just a less safe database.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; read every setting back at startup, and refuse to start if one doesn’t match.&lt;/p&gt;
&lt;h2 id=&quot;bonus-checking-the-maths&quot;&gt;Bonus: checking the maths&lt;/h2&gt;
&lt;p&gt;The plan was full of numbers, so we checked every one with a script. All correct.&lt;/p&gt;
&lt;p&gt;One limit nobody had written down: with the default settings, only &lt;strong&gt;12 monitors&lt;/strong&gt; can point at the same server before the monitor starts saying no. That’s the first wall a real user would hit.&lt;/p&gt;
&lt;h2 id=&quot;what-it-doesn-t-mean&quot;&gt;What it doesn’t mean&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;None of these are bugs in those libraries. They’re sensible choices for general use, just wrong for a monitor.&lt;/li&gt;
&lt;li&gt;These facts are for these exact versions, and they get re-checked on every upgrade.&lt;/li&gt;
&lt;li&gt;At this point there was zero monitor code. This was all reading.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-takeaway&quot;&gt;The takeaway&lt;/h2&gt;
&lt;p&gt;The libraries were fine. My assumptions weren’t.&lt;/p&gt;
&lt;p&gt;Reading first turned four “it probably works like this” into four tests to write before the code that needs them.&lt;/p&gt;
</description>
      </item>
      <item>
          <title>I made an AI read the entire Rust Book. Then I tried to make it fail.</title>
          <pubDate>Wed, 14 Oct 2026 00:00:00 +0000</pubDate>
          <author>404human</author>
          <link>https://404human.dev/posts/i-made-an-ai-read-the-rust-book/</link>
          <guid>https://404human.dev/posts/i-made-an-ai-read-the-rust-book/</guid>
          <description xml:base="https://404human.dev/posts/i-made-an-ai-read-the-rust-book/">&lt;p&gt;AI can write Rust that looks perfect.&lt;/p&gt;
&lt;p&gt;That’s exactly what worries me.&lt;/p&gt;
&lt;p&gt;Rust is the language where “looks right” and “is right” can be miles apart. Ownership, lifetimes, error handling, crates that change their API between versions. That’s where AI-written Rust usually falls apart. You get code that’s confident, clean and wrong.&lt;/p&gt;
&lt;p&gt;So I tried something. I asked Claude (in Claude Code) to do what a serious human learner would do: read the whole official Rust Book, take notes, sit exams, grade itself, and fix whatever it got wrong.&lt;/p&gt;
&lt;p&gt;Then I asked the question that actually matters: &lt;strong&gt;does any of it carry over to real projects?&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;step-1-read-the-whole-book&quot;&gt;Step 1: Read the whole book&lt;/h2&gt;
&lt;p&gt;Not a summary. The full official Rust Book, all 23 parts, with examples checked against a real compiler (Rust 1.99).&lt;/p&gt;
&lt;p&gt;Everything went into one main file: the rules, chapter notes, memory measurements, exam scores, and a log of every mistake.&lt;/p&gt;
&lt;h2 id=&quot;step-2-the-theory-exam&quot;&gt;Step 2: The theory exam&lt;/h2&gt;
&lt;p&gt;Separate AI “examiner” agents wrote 138 questions across 9 exams, with hidden answer keys checked against the compiler.&lt;/p&gt;
&lt;p&gt;Score: &lt;strong&gt;136.5 / 138 (98.9%).&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The 1.5 points it lost were almost funny:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It miscounted a line number because it skipped a blank line.&lt;/li&gt;
&lt;li&gt;It copied a line position from the book’s example instead of the code in front of it.&lt;/li&gt;
&lt;li&gt;It quoted an error code that Rust has since retired.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Before grading, it predicted its own score: 133. It came in higher. I’ll take a humble AI over an overconfident one.&lt;/p&gt;
&lt;h2 id=&quot;step-3-write-code-with-no-compiler&quot;&gt;Step 3: Write code with no compiler&lt;/h2&gt;
&lt;p&gt;This is where most AI coding falls over.&lt;/p&gt;
&lt;p&gt;36 coding tasks. 261 hidden tests it couldn’t see. And one hard rule: &lt;strong&gt;no compiler&lt;/strong&gt;. No running the code, no checking, no “let me try that again”. Write it once and hand it in, like a whiteboard interview.&lt;/p&gt;
&lt;p&gt;The topics were the hard ones: ownership and lifetimes, error handling across a whole app, threads, async, unsafe code, and real crates like tokio, serde, clap and nom.&lt;/p&gt;
&lt;p&gt;Result: &lt;strong&gt;254 / 261 tests passed on the first try (97.3%).&lt;/strong&gt; 35 of 36 tasks compiled.&lt;/p&gt;
&lt;p&gt;All 7 lost tests came from the one task that didn’t compile. The linter also flagged 4 style issues. In everything that did compile, there wasn’t a single logic error. After fixes: 261 / 261, zero warnings.&lt;/p&gt;
&lt;p&gt;But that one failure turned out to be the best part of the whole experiment.&lt;/p&gt;
&lt;h2 id=&quot;the-bug-that-taught-me-something&quot;&gt;The bug that taught me something&lt;/h2&gt;
&lt;p&gt;Claude gave a helper method a name that Rust’s built-in &lt;code&gt;Iterator&lt;/code&gt; already uses. Rust picked the built-in one. In the exam that meant a compile error and a quick fix.&lt;/p&gt;
&lt;p&gt;But I’d asked it to understand &lt;em&gt;why&lt;/em&gt; a mistake matters, not just patch it. So it built an experiment to see what the same mistake looks like in a real project.&lt;/p&gt;
&lt;p&gt;Here’s a simple job queue with its own &lt;code&gt;count&lt;/code&gt; method. The queue also implements &lt;code&gt;Iterator&lt;/code&gt;, so it can hand out jobs one at a time:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: light-dark(#24292E, #E1E4E8); background-color: light-dark(#FFFFFF, #24292E);&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt;impl&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #B392F0);&quot;&gt; Jobs&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6A737D, #6A737D);&quot;&gt;    //&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6A737D, #6A737D);&quot;&gt; &amp;quot;How many jobs are waiting?&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt;    fn&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #B392F0);&quot;&gt; count&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;self&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt; -&amp;gt;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #B392F0);&quot;&gt; usize&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt;.&lt;/span&gt;&lt;span&gt;items&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #B392F0);&quot;&gt;len&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt;    fn&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #B392F0);&quot;&gt; summary&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;self&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt; -&amp;gt;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #B392F0);&quot;&gt; String&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #B392F0);&quot;&gt;        format!&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt; jobs waiting&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #B392F0);&quot;&gt;count&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6A737D, #6A737D);&quot;&gt; //&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6A737D, #6A737D);&quot;&gt; calls the count above&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt;    fn&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #B392F0);&quot;&gt; report&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt;mut&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt; self&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt; -&amp;gt;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #B392F0);&quot;&gt; usize&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #B392F0);&quot;&gt;count&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6A737D, #6A737D);&quot;&gt; //&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6A737D, #6A737D);&quot;&gt; same words... calls Iterator::count instead&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: light-dark(#24292E, #E1E4E8); background-color: light-dark(#FFFFFF, #24292E);&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;3 jobs waiting&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;report() says 3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;0 jobs waiting&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;report()&lt;/code&gt; returned the right number. And it quietly emptied the whole queue to get it.&lt;/p&gt;
&lt;p&gt;Same two words, &lt;code&gt;self.count()&lt;/code&gt;, two different methods. Which one runs depends on whether you’re inside a &lt;code&gt;&amp;amp;self&lt;/code&gt; method or a &lt;code&gt;&amp;amp;mut self&lt;/code&gt; one.&lt;/p&gt;
&lt;p&gt;It compiles with zero warnings. The default linter says nothing. Even the extra-strict lint groups miss it. A test that only checks the returned number passes.&lt;/p&gt;
&lt;p&gt;That’s silent data loss. The kind of bug that makes it to production.&lt;/p&gt;
&lt;p&gt;It’s like having two people named Sam in the same room. You call “Sam”, the wrong one answers, and nobody notices, because someone did answer.&lt;/p&gt;
&lt;p&gt;The catch is one opt-in lint, &lt;code&gt;clippy::same_name_method&lt;/code&gt;, which flags it straight away. It’s now on Claude’s checklist for every Rust project.&lt;/p&gt;
&lt;h2 id=&quot;step-4-prove-it-wasn-t-memorized&quot;&gt;Step 4: “Prove it wasn’t memorized”&lt;/h2&gt;
&lt;p&gt;Fixing a mistake is easy. Not repeating it in a different shape is the real test.&lt;/p&gt;
&lt;p&gt;So a fresh examiner read the full list of past mistakes and built 8 brand-new tasks, each hiding one of those traps in disguise:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the same name clash, now hiding behind a type that acts like a string&lt;/li&gt;
&lt;li&gt;text positions counted in three different units (bytes, characters and UTF-16)&lt;/li&gt;
&lt;li&gt;callbacks that could deadlock while a lock is held&lt;/li&gt;
&lt;li&gt;code that had to build on an older Rust version, where the “modern” suggestion doesn’t exist yet&lt;/li&gt;
&lt;li&gt;a newer version of a real crate, whose API differs from the older version also installed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For each task the examiner also wrote a &lt;strong&gt;decoy&lt;/strong&gt;: the plausible, confident answer an AI typically writes. The hidden tests had to catch it.&lt;/p&gt;
&lt;p&gt;The decoys passed between 0 and 4 tests per task. Some deadlocked. Some didn’t even compile.&lt;/p&gt;
&lt;p&gt;Claude, first try, no compiler: &lt;strong&gt;59 / 59.&lt;/strong&gt; Zero linter warnings.&lt;/p&gt;
&lt;h2 id=&quot;plot-twist-the-examiner-caught-claude-s-notes-being-wrong&quot;&gt;Plot twist: the examiner caught Claude’s notes being wrong&lt;/h2&gt;
&lt;p&gt;While building that test, the examiner found a misleading line in Claude’s own notes about how Rust 2024 handles locks inside &lt;code&gt;if let&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The note was technically true, but it left out the part that matters: inside the &lt;code&gt;if let&lt;/code&gt; block, the lock is still held. Someone following that note could write code that freezes.&lt;/p&gt;
&lt;p&gt;Claude didn’t argue. It tested it on the compiler, confirmed the examiner was right, and fixed it in all three places it appeared.&lt;/p&gt;
&lt;p&gt;Honestly, this is the part I value most. It caught a mistake in its own knowledge, not just in its code.&lt;/p&gt;
&lt;h2 id=&quot;what-carries-over-to-real-projects&quot;&gt;What carries over to real projects&lt;/h2&gt;
&lt;p&gt;The scores aren’t the point. The point is what happens next time.&lt;/p&gt;
&lt;p&gt;Every lesson is written down three ways: &lt;strong&gt;what went wrong, why it matters on a real project, and how to prevent it.&lt;/strong&gt; Each one is backed by an experiment that proves it.&lt;/p&gt;
&lt;p&gt;And there’s now a “definition of done” that loads in every Rust project I open with Claude Code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tests pass&lt;/li&gt;
&lt;li&gt;the linter runs with zero warnings, including the name-clash check&lt;/li&gt;
&lt;li&gt;the project’s minimum Rust version is checked before using newer features&lt;/li&gt;
&lt;li&gt;crate code is read from the exact installed version, never from memory&lt;/li&gt;
&lt;li&gt;no lock or borrow is held while calling someone else’s code&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-honest-part&quot;&gt;The honest part&lt;/h2&gt;
&lt;p&gt;This isn’t proof that AI writes perfect Rust now.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The examiners were AI agents, not human Rust experts. Everything was checked against the real compiler, but they’re still AI.&lt;/li&gt;
&lt;li&gt;These were library-sized tasks, not a huge production codebase. Real projects will throw up new problems.&lt;/li&gt;
&lt;li&gt;It still made mistakes. That’s the whole point. What changed is what happens after one.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-takeaway&quot;&gt;The takeaway&lt;/h2&gt;
&lt;p&gt;What separates a good developer from an average one isn’t never making mistakes. It’s not making the same mistake twice, and knowing &lt;em&gt;why&lt;/em&gt; it was a mistake in the first place.&lt;/p&gt;
&lt;p&gt;Turns out that’s a pretty good bar for AI too.&lt;/p&gt;
</description>
      </item>
    </channel>
</rss>
