<?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: Jennifer Smith</title>
    <description>The latest articles on DEV Community by Jennifer Smith (@jenatechio).</description>
    <link>https://dev.to/jenatechio</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%2F4055605%2Fab197679-7b2f-4d50-af8c-3619daab3bba.png</url>
      <title>DEV Community: Jennifer Smith</title>
      <link>https://dev.to/jenatechio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jenatechio"/>
    <language>en</language>
    <item>
      <title>A Dead-Man's Switch for Scheduled Jobs (Because Silent Failures Rot Data)</title>
      <dc:creator>Jennifer Smith</dc:creator>
      <pubDate>Thu, 30 Jul 2026 19:40:21 +0000</pubDate>
      <link>https://dev.to/jenatechio/a-dead-mans-switch-for-scheduled-jobs-because-silent-failures-rot-data-3nmh</link>
      <guid>https://dev.to/jenatechio/a-dead-mans-switch-for-scheduled-jobs-because-silent-failures-rot-data-3nmh</guid>
      <description>&lt;p&gt;A scheduled job has two failure modes. The loud one throws an error and your alerting catches it. The quiet one just stops running, and nothing catches it, because your alerting is wired to the job itself. No job, no error, no alert. The data it maintains gets staler by the day and every downstream system keeps serving it like nothing's wrong.&lt;/p&gt;

&lt;p&gt;I run a handful of production systems by myself. One of them is a niche content site with a curated data feed behind it, plus a release-tracking pipeline on Cloudflare Workers and D1, plus some geolocation alerting. There's no ops team. The quiet failure mode is the one that actually bit me.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I found out
&lt;/h2&gt;

&lt;p&gt;The curation job that refreshes the feed died without a sound. No exception. Nothing in the logs that would make you stop scrolling. The site kept serving the last good copy of the feed, so every health check I had stayed green — the checks were all downstream of the dead job, and downstream was fine.&lt;/p&gt;

&lt;p&gt;I found out days later, reading my own site as a user, thinking huh, this hasn't changed in a while.&lt;/p&gt;

&lt;p&gt;My first instinct was to go add error handling, which was the wrong instinct, because the job didn't error. What I'd actually built was alerting for when things fail. I had nothing for when things stop happening. Took me an embarrassingly long time to see that those are different problems.&lt;/p&gt;

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

&lt;p&gt;A dead-man's switch flips the alerting relationship around. The job doesn't report failure; it has to keep proving success. Silence is what fires the alarm.&lt;/p&gt;

&lt;p&gt;Two parts, both small.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The job stamps every successful run.&lt;/strong&gt; Last thing before exit, write a timestamp somewhere cheap — a KV key, a D1 row, or in my case a field right in the feed file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"generated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-30T14:00:11Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only on success. After the output is actually verified. Not in a &lt;code&gt;finally&lt;/code&gt; block — a stamp that gets written on failure is worse than no stamp, and yes, I initially had it in the wrong place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A separate watcher checks the stamp's age.&lt;/strong&gt; Separate is the load-bearing word. Different runtime, different schedule, different failure domain. Mine is a tiny scheduled Worker on its own cron that reads the stamp and does one comparison:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ageHours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stamp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ageHours&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_AGE_HOURS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Feed is stale: last success &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ageHours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;h ago`&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;Job dies, watcher fires. Job hangs, watcher fires. I fat-finger the cron expression in a deploy, watcher fires. It doesn't know why the stamp is old and doesn't need to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The threshold is the hard part
&lt;/h2&gt;

&lt;p&gt;Not the code. The number.&lt;/p&gt;

&lt;p&gt;My instinct was to set the threshold just above the job's schedule — job runs daily, alert at 26 hours, right? That version paged me constantly, because my feed is &lt;em&gt;curated&lt;/em&gt;: the job runs daily but the content only genuinely changes when there's something worth adding, which some weeks is barely at all. So I'd get a stale alert, check it, find nothing wrong, dismiss it. After the third or fourth round of that I'd trained myself to ignore the watcher, at which point it protected nothing.&lt;/p&gt;

&lt;p&gt;The threshold I actually run on that feed now is 720 hours. Thirty days. That number looks absurd next to a daily cron until you accept that the thing being monitored is the &lt;em&gt;content's&lt;/em&gt; rhythm, not the job's. The rule I eventually landed on: set it long enough that an alert is always worth investigating, short enough that the damage window is one you can live with. For a different system of mine — event scraping, where a dead day matters — the threshold is tight. Same pattern, wildly different number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the alert say what broke
&lt;/h2&gt;

&lt;p&gt;The notification goes to a Discord channel I actually read. It carries three things: which system, how stale, where to look first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠️ feed stale: last success 39.2h ago (threshold 24h).
Check: worker cron logs → feed generator → upstream source.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 7 a.m., "something is wrong somewhere" and "this specific thing is wrong, start here" are very different messages to receive. When you're the whole on-call rotation, the second one is a two-minute fix. The first one is a knot in your stomach until the weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;p&gt;Basically zero. The watcher is a scheduled Worker on the free tier and the stamp is a few bytes. An hour to build the first one, minutes for each system after.&lt;/p&gt;

&lt;p&gt;Since the stamps and watchers went in, no silent failure has made it past one threshold window. The loud failures were never the problem. If your alerting only fires when jobs fail, it's worth checking what happens when one simply stops.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>serverless</category>
      <category>cloudflare</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>My Auto-Publish Pipeline Shipped a Two-Year-Old News Story. Here's the Fix — All Three Layers of It.</title>
      <dc:creator>Jennifer Smith</dc:creator>
      <pubDate>Thu, 30 Jul 2026 19:39:23 +0000</pubDate>
      <link>https://dev.to/jenatechio/my-auto-publish-pipeline-shipped-a-two-year-old-news-story-heres-the-fix-all-three-layers-of-it-4750</link>
      <guid>https://dev.to/jenatechio/my-auto-publish-pipeline-shipped-a-two-year-old-news-story-heres-the-fix-all-three-layers-of-it-4750</guid>
      <description>&lt;p&gt;I run a content pipeline that finds industry news, has an LLM judge score it, and publishes what clears the bar. Fully automated, Cloudflare Workers and D1, one operator: me. It runs daily and the whole point is that I don't babysit it.&lt;/p&gt;

&lt;p&gt;In July it published a news story from 2024 as if it had just happened.&lt;/p&gt;

&lt;p&gt;Nobody was harmed. One item, niche site, caught on a routine quality check within a few days, unpublished. But "the robot posted two-year-old news and nothing stopped it" is the kind of failure that quietly costs a site its credibility, and it deserved a real postmortem instead of a shrug. What went wrong turned out to be more interesting than I expected, and the fix ended up being three layers instead of the one I planned to write.&lt;/p&gt;

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

&lt;p&gt;The pipeline already had two mechanisms that should have caught this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There was a recency check.&lt;/strong&gt; But when I built it, the stale-content problem I was seeing was old product releases resurfacing on aggregator sites. So the year-sanity check ran against product items. News items took a different path through the gate and never met it. The check had no bug. It was standing at the wrong door.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There was an LLM judge.&lt;/strong&gt; It scores every item before publication: relevant? interesting? worth the reader's time? It read the 2024 story and scored it well — because the story &lt;em&gt;was&lt;/em&gt; relevant and interesting. I had never asked the judge whether the story was current. That question wasn't in the rubric, so it wasn't asked. A human editor has a reflexive "hang on, when is this from?" An LLM judge has exactly the reflexes you wrote down and none you didn't.&lt;/p&gt;

&lt;p&gt;Upstream, an aggregator had resurfaced the old story with a fresh feed timestamp. Everything downstream took the feed's word for it. Two guards, both working as designed, both blind to this one.&lt;/p&gt;

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

&lt;p&gt;My first draft of the fix was one line: extend the year check to news items. Done, ship it. I've shipped enough one-line fixes to know that the incident you just had is rarely the exact incident you'll have next, so it became three thinner layers instead. Each one covers a different way the other two fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: widen the deterministic check.&lt;/strong&gt; The year-sanity check now runs against every content type that passes the gate, not just the type that misbehaved historically. When a deterministic guard misses, check its scope before its logic — in my experience the case almost always walked &lt;em&gt;around&lt;/em&gt; the guard, not through it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: put recency in the judge's rubric.&lt;/strong&gt; The judge now explicitly evaluates currency: when did this event happen, does its age undermine its value, penalize items whose event date sits far behind the feed date. This is the layer that catches what rules can't express. It only works for dimensions you remember to name. Rubrics are like tests that way — they encode the failures you've already imagined, and nothing else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3: extract the event date from the content itself.&lt;/strong&gt; This one goes at the root cause: trusting upstream metadata. A resurfaced story arrives wearing a fresh timestamp; the packaging lies even when the content doesn't. The pipeline now tries to pull the actual event date out of the item's text and compares it to the feed's claimed freshness. Disagreement gets flagged for me instead of published. It's also the flakiest of the three layers — date extraction from prose fails in fun ways — which is exactly why the two blunter layers stand in front of it.&lt;/p&gt;

&lt;p&gt;Any one of these would have caught July's incident. I've watched each &lt;em&gt;kind&lt;/em&gt; of layer fail before, though: deterministic checks miss on scope, judges miss on rubric gaps, extraction misses on weird input. So it's all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone running auto-publish
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Your failure modes are public.&lt;/strong&gt; A bad batch job embarrasses you in a log file. A bad publish embarrasses you in front of readers. Budget your quality-gate effort by audience size, not code size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM judges have no instincts.&lt;/strong&gt; Every implicit check a human editor does for free has to be written into the rubric explicitly. The judge will hand out perfect scores along every axis you specified while the item fails one you didn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't trust a timestamp you didn't compute.&lt;/strong&gt; Feed metadata describes the feed's behavior, not the content's age. If freshness matters, derive it from the content.&lt;/p&gt;

&lt;p&gt;The item came down, the three layers shipped the same week, and the pipeline went back to running unattended — which is the whole point. One person can run automated publishing. The tax is that every incident has to buy a structural fix, not a patch for the instance. Patch the instance and you've just scheduled the next one.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>serverless</category>
      <category>postmortem</category>
    </item>
  </channel>
</rss>
