<?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: Ramon</title>
    <description>The latest articles on DEV Community by Ramon (@ramon_galego).</description>
    <link>https://dev.to/ramon_galego</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3802529%2F59f25b83-e4ea-4eb0-89ae-3df33f11b329.jpg</url>
      <title>DEV Community: Ramon</title>
      <link>https://dev.to/ramon_galego</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ramon_galego"/>
    <language>en</language>
    <item>
      <title>Your LangSmith Traces Are Not an Audit Trail</title>
      <dc:creator>Ramon</dc:creator>
      <pubDate>Fri, 10 Apr 2026 19:02:04 +0000</pubDate>
      <link>https://dev.to/ramon_galego/your-langsmith-traces-are-not-an-audit-trail-3d63</link>
      <guid>https://dev.to/ramon_galego/your-langsmith-traces-are-not-an-audit-trail-3d63</guid>
      <description>&lt;p&gt;You have LangSmith set up. You can see every prompt, every token, every span. Your traces are clean, your latency charts are green, and you can replay any run from the last 30 days.&lt;/p&gt;

&lt;p&gt;When your compliance officer asks what your agent did on March 14th, you send them a LangSmith link.&lt;/p&gt;

&lt;p&gt;They come back with more questions. And you realise you answered the wrong question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability tools are built for engineers
&lt;/h2&gt;

&lt;p&gt;LangSmith, Langfuse, Arize, Helicone. These are genuinely useful tools. They exist to help you debug prompts, track costs, measure latency, and understand why a chain returned something unexpected.&lt;/p&gt;

&lt;p&gt;They are built for the question: why did this not work the way I expected?&lt;/p&gt;

&lt;p&gt;That is an engineering question. It gets asked during development, during an incident, during a postmortem. The audience is you and your team. The output is a fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit trails are built for auditors
&lt;/h2&gt;

&lt;p&gt;An audit trail exists to answer a different question: can you prove what your agent did, and that it was authorised to do it?&lt;/p&gt;

&lt;p&gt;That question gets asked by a compliance officer, a regulator, a customer's legal team, or an external auditor. It might get asked six months from now. The audience is not your engineering team. The output is evidence.&lt;/p&gt;

&lt;p&gt;The distinction matters because the two tools are built with completely different constraints in mind.&lt;/p&gt;

&lt;p&gt;Observability tools are optimised for developer experience. Fast search, good visualisation, easy filtering. Retention is typically short because storage is expensive and the main use case is recent debugging. The data lives in a database the vendor controls. If you delete a trace, it is gone.&lt;/p&gt;

&lt;p&gt;An audit trail needs to be the opposite. Long retention by default. Immutable records that cannot be edited or deleted after the fact. Cryptographic proof that what you are showing today is exactly what was recorded at the time. Readable by a non-technical person. Something you can hand to an auditor without a 20-minute explanation of what a span is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The specific things that are missing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Immutability.&lt;/strong&gt; LangSmith stores your traces in a database. That database can be written to. Records can be deleted. There is no cryptographic proof that a trace you show an auditor today is identical to what was recorded six months ago. A mutable log is not evidence. It is an assertion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chain of custody.&lt;/strong&gt; Observability traces show you what the LLM did. They typically do not capture the full sequence of tool calls, external API calls, human approval steps, and downstream effects that make up an agent's actual action in the world. An auditor does not care about your token counts. They care about what your agent did to real data and real systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retention guarantees.&lt;/strong&gt; The EU AI Act requires six months minimum for high-risk systems. HIPAA requires six years. Most observability tools default to 30 or 90 days. You can pay for longer, but retention is not the same as an archived, legally defensible record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-technical readability.&lt;/strong&gt; Traces are structured for developers. They are full of span IDs, model names, raw JSON, and timing data. If your compliance team needs to understand what your agent did, they cannot read a LangSmith trace without help. An audit trail needs to be legible to the person asking the question.&lt;/p&gt;

&lt;h2&gt;
  
  
  You probably need both
&lt;/h2&gt;

&lt;p&gt;This is not an argument to stop using observability tools. Use them. They are the right tool for debugging and performance monitoring.&lt;/p&gt;

&lt;p&gt;But they should not be your answer when someone asks you to prove what your agent did. They were never designed to be that answer, and treating them as one creates a compliance gap that will surface at the worst possible time.&lt;/p&gt;

&lt;p&gt;The question to ask about your current setup: if an auditor asked you right now to produce a tamper-proof record of every action your agent took in a specific session three months ago, could you do it?&lt;/p&gt;

&lt;p&gt;If the answer is "we would pull the LangSmith traces," you have observability. You do not have an audit trail.&lt;/p&gt;




&lt;p&gt;AgentReceipt gives your AI agents a tamper-proof audit trail with hash-chained records anchored to a public transparency log. Three lines of code. No infrastructure to manage. &lt;a href="https://agentreceipt.co" rel="noopener noreferrer"&gt;agentreceipt.co&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Cron Job That Lied to You</title>
      <dc:creator>Ramon</dc:creator>
      <pubDate>Fri, 10 Apr 2026 18:50:03 +0000</pubDate>
      <link>https://dev.to/ramon_galego/the-cron-job-that-lied-to-you-26nh</link>
      <guid>https://dev.to/ramon_galego/the-cron-job-that-lied-to-you-26nh</guid>
      <description>&lt;p&gt;Your nightly backup ran at 2 AM. The ping arrived on schedule. No alerts, no incidents, nothing in your dashboard but a row of green checkmarks.&lt;/p&gt;

&lt;p&gt;The backup file was empty.&lt;/p&gt;

&lt;p&gt;The job ran. It checked in. It lied to you.&lt;/p&gt;

&lt;p&gt;Basic heartbeat monitoring solves one problem: knowing whether your job ran at all. If the ping stops arriving, you get alerted. That is genuinely useful and it catches a whole class of failures. But there is a quieter category of failure that heartbeat monitoring alone does not cover. The job shows up, does its ping, and something is still wrong.&lt;/p&gt;

&lt;p&gt;Here are the four ways that happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The job finished, but so did another copy of it
&lt;/h2&gt;

&lt;p&gt;Your sync job runs every five minutes and usually completes in about 90 seconds. One night the database gets slow. The job starts taking six minutes. Cron does not know this. At the five minute mark it fires a new instance. Now you have two copies of the same job running at the same time, both reading and writing to the same tables.&lt;/p&gt;

&lt;p&gt;Each one eventually finishes and pings success. Your monitor sees two pings and is perfectly happy. Your data has duplicate records in it.&lt;/p&gt;

&lt;p&gt;This is what overlap detection catches. When you send a start ping at the beginning of a job, PulseMon tracks whether the previous run finished before the new one begins. If it did not, you get an immediate alert. Not after the data is corrupted. At the moment the second instance starts.&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;# Start of job&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; https://pulsemon.dev/api/ping/sync-job?status&lt;span class="o"&gt;=&lt;/span&gt;start

&lt;span class="c"&gt;# ... your job logic ...&lt;/span&gt;

&lt;span class="c"&gt;# End of job&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; https://pulsemon.dev/api/ping/sync-job
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The job finished, but it took way longer than it should have
&lt;/h2&gt;

&lt;p&gt;This one is subtle because it looks completely fine from the outside. The job ran, it finished, it pinged. But it usually takes four minutes and today it took 47.&lt;/p&gt;

&lt;p&gt;That is almost always a sign something upstream is struggling. A slow query. A downstream API responding at a crawl. A dataset that has grown past a threshold your job was not designed for. The job will probably fail completely within the next few runs. Or it will keep completing slowly, quietly degrading until it starts missing its window.&lt;/p&gt;

&lt;p&gt;Duration thresholds let you set a ceiling on how long a job should take. If the job checks in successfully but blew past that ceiling, you get alerted. The job succeeded by every technical measure and you still get notified, because the duration is itself a signal worth acting on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The job failed, but your monitor was going to wait it out
&lt;/h2&gt;

&lt;p&gt;Without explicit failure signalling, heartbeat monitoring works on absence. You set an interval, and if no ping arrives by the deadline, the monitor goes down and you get alerted.&lt;/p&gt;

&lt;p&gt;The problem is the window. If your job is supposed to run every 30 minutes and it fails immediately, you might not find out for 30 minutes. Plus the grace period. That is a long time to wait on a payment processor job or an order fulfilment worker.&lt;/p&gt;

&lt;p&gt;The fix is a fail ping. When your job catches an error, it can tell PulseMon directly instead of just going quiet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;run_invoice_job&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://pulsemon.dev/api/ping/invoice-job&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://pulsemon.dev/api/ping/invoice-job?status=fail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fail ping fires an immediate alert. You find out in seconds, not when the deadline expires.&lt;/p&gt;

&lt;h2&gt;
  
  
  The job missed its deadline and you have no idea why
&lt;/h2&gt;

&lt;p&gt;This one is not a lie exactly. The job did not check in, you got alerted, something is clearly wrong. But then what? You SSH into the server, check the logs, and try to piece together what happened from whatever the job managed to write before it died.&lt;/p&gt;

&lt;p&gt;The ping body changes this. You can POST your job's output with the ping, and when the alert fires it includes that output. The failure context comes to you instead of you going to find it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;OUTPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;your-job-command 2&amp;gt;&amp;amp;1&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;STATUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$STATUS&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      https://pulsemon.dev/api/ping/your-job
&lt;span class="k"&gt;else
    &lt;/span&gt;curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      https://pulsemon.dev/api/ping/your-job?status&lt;span class="o"&gt;=&lt;/span&gt;fail
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your alert email contains the last thing the job printed before it went wrong. No SSH required.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a successful ping actually means
&lt;/h2&gt;

&lt;p&gt;A ping tells you the job reached the line of code that fires the request. That is it. It says nothing about whether the job ran in isolation, whether it finished in a reasonable time, or whether it failed and told you immediately.&lt;/p&gt;

&lt;p&gt;These four features are not replacements for heartbeat monitoring. They sit on top of it. A ping is still the foundation. But a ping on its own is a pretty low bar for "everything is fine."&lt;/p&gt;

&lt;p&gt;The jobs that bite you worst are not the ones that go completely dark. Those are obvious. The hard ones are the jobs that keep showing up, keep checking in, and are quietly doing something wrong every single time.&lt;/p&gt;




&lt;p&gt;PulseMon supports start, success, and fail pings, duration thresholds, overlap detection, and ping body in alerts on all plans. Free tier includes 30 monitors. No credit card required. &lt;a href="https://pulsemon.dev" rel="noopener noreferrer"&gt;pulsemon.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>webdev</category>
      <category>devops</category>
      <category>backend</category>
    </item>
    <item>
      <title>Your AI agent just took an action. Do you know what it did?</title>
      <dc:creator>Ramon</dc:creator>
      <pubDate>Sun, 22 Mar 2026 18:35:58 +0000</pubDate>
      <link>https://dev.to/ramon_galego/your-ai-agent-just-took-an-action-do-you-know-what-it-did-316j</link>
      <guid>https://dev.to/ramon_galego/your-ai-agent-just-took-an-action-do-you-know-what-it-did-316j</guid>
      <description>&lt;p&gt;A few months ago, a fintech company's accounts payable agent approved and triggered a $47,000 payment to a vendor that had been flagged for fraud two weeks earlier. The flag was in the system. The agent never saw it. By the time anyone noticed, the money was gone.&lt;/p&gt;

&lt;p&gt;The company had logs. Technically. They had server logs, database logs, error logs. What they didn't have was a clear record of what the agent saw, what it decided, and why it sent that payment. When their auditors asked, the engineering team spent three days piecing together a timeline from scattered log files that were never designed to answer that question.&lt;/p&gt;

&lt;p&gt;This is not an edge case. This is what happens when you put AI agents into production without thinking about accountability.&lt;/p&gt;




&lt;h2&gt;
  
  
  Agents are different from software
&lt;/h2&gt;

&lt;p&gt;Traditional software is deterministic. If a bug causes a wrong transaction, you look at the code, find the bug, fix it. The behavior is reproducible and the cause is traceable.&lt;/p&gt;

&lt;p&gt;AI agents don't work like that. They reason. They make judgment calls. Two identical inputs can produce different outputs depending on context, model temperature, and what happened in previous steps. When something goes wrong, "look at the code" doesn't give you answers. You need to know what the agent actually did, step by step, in that specific run.&lt;/p&gt;

&lt;p&gt;This is a fundamentally new problem. And regulators are starting to notice.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the law says now
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The EU AI Act
&lt;/h3&gt;

&lt;p&gt;The EU AI Act became enforceable in stages through 2025 and 2026. The full weight of it lands on August 2, 2026.&lt;/p&gt;

&lt;p&gt;For anyone deploying AI in high-risk categories, Article 19 is the one to know. It requires providers of high-risk AI systems to maintain automatically generated logs for a minimum of six months. Longer in some sectors. The logs must be detailed enough to reconstruct what the system did and why.&lt;/p&gt;

&lt;p&gt;High-risk categories include: employment and HR decisions, credit and financial services, healthcare, education, law enforcement, and critical infrastructure. If your AI agent touches any of those areas, Article 19 applies to you.&lt;/p&gt;

&lt;p&gt;The fines for non-compliance go up to 30 million euros or 6% of global annual revenue, whichever is higher. These are not theoretical numbers. The EU has shown it will enforce them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The US picture
&lt;/h3&gt;

&lt;p&gt;The US has no single federal AI law yet. But the regulatory pressure is real and it comes from multiple directions at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SOC 2&lt;/strong&gt; is the de facto standard for B2B SaaS security. If you're selling to enterprise customers, they will ask for your SOC 2 report. Auditors evaluating SOC 2 compliance specifically look for activity logs that show who or what accessed what, when, and what they did. An AI agent that sends emails or triggers payments on your behalf is a system that SOC 2 auditors will want to see logs for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HIPAA&lt;/strong&gt; applies to any system handling protected health information. If your agent reads patient records, schedules appointments, or processes healthcare data in any form, HIPAA requires six-year retention of activity logs. Six years. Most teams think about HIPAA in terms of data encryption and access controls, but the logging requirement is just as strict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SOX and SEC rules&lt;/strong&gt; govern financial reporting and trading. If your agents are involved in expense approvals, transaction processing, or financial data handling, you need to be able to prove they followed the rules. Not just that the rules existed, but that they were followed, step by step, in each specific instance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State laws&lt;/strong&gt; are filling the federal gap. Colorado's AI Act took effect in 2026, requiring reasonable care to prevent algorithmic discrimination and documentation to prove it. California has multiple overlapping AI transparency requirements now in effect. Texas passed TRAIGA on January 1, 2026. These laws are moving fast and the trend is clearly toward more documentation, not less.&lt;/p&gt;

&lt;h3&gt;
  
  
  The common thread
&lt;/h3&gt;

&lt;p&gt;Across all of these frameworks, the requirement is the same: prove what your AI did. Not in general. In the specific instance your auditor is asking about.&lt;/p&gt;

&lt;p&gt;"Our agent follows these rules" is not an answer. "Here is a timestamped, immutable record of every action the agent took on March 14 at 2:47pm, here is what it saw, here is what it decided, and here is why" is an answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with existing tools
&lt;/h2&gt;

&lt;p&gt;Most teams are using one of three approaches to deal with this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Application logs.&lt;/strong&gt; Standard server logs capture requests and responses but not reasoning. They tell you the agent made a call. They don't tell you what it was thinking. When something goes wrong, you're reconstructing a timeline from logs that were never designed to answer compliance questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM observability tools&lt;/strong&gt; like Langfuse or LangSmith are genuinely useful for debugging. They capture traces, spans, token counts, and latency. They're built for engineers who want to understand why a prompt failed or why costs spiked. They are not built for the compliance officer asking what your agent did on Tuesday.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing.&lt;/strong&gt; More common than people admit. Teams move fast, get agents into production, and assume logging can be sorted out later. Later is when the auditor arrives.&lt;/p&gt;

&lt;p&gt;The gap isn't technical. The tools to capture logs exist. The gap is that nobody is building for the people who need to read those logs.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a real audit trail actually needs
&lt;/h2&gt;

&lt;p&gt;When regulators or auditors ask what your agent did, they need specific things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A complete timeline.&lt;/strong&gt; Every action in sequence. Not just the LLM calls but the tool calls, the decisions, the data accessed, the outputs produced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The reasoning, not just the result.&lt;/strong&gt; Why did the agent approve that payment? What criteria did it apply? What did it see that led it to that conclusion?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human review steps.&lt;/strong&gt; If a person signed off before the agent proceeded, that needs to be in the record too. The full chain of accountability, not just the automated parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Immutability.&lt;/strong&gt; A log you can edit is not an audit trail. The record needs to be append-only with cryptographic proof that nothing was changed after the fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Readability.&lt;/strong&gt; Your compliance team is not going to read JSON traces. The record needs to be something a non-technical person can actually understand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retention.&lt;/strong&gt; Six months minimum for EU AI Act. Six years for HIPAA. The record needs to exist when someone asks for it, not just when it's convenient.&lt;/p&gt;




&lt;h2&gt;
  
  
  The window is closing
&lt;/h2&gt;

&lt;p&gt;The EU AI Act enforcement deadline is August 2026. That is not far away. Companies that have been running agents in production without audit trails are going to face a choice: retrofit compliance into systems that were never designed for it, or get ahead of it now.&lt;/p&gt;

&lt;p&gt;Getting ahead of it now is much cheaper than getting ahead of it in July 2026 with an auditor waiting.&lt;/p&gt;

&lt;p&gt;The companies that take compliance seriously from the start will close enterprise deals faster. They will pass security reviews without delays. They will have answers when auditors ask questions. And when something goes wrong, they will know exactly what happened.&lt;/p&gt;

&lt;p&gt;The companies that wait will be doing log archaeology at the worst possible time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;In a follow-up post, I'll cover how I built &lt;a href="https://agentreceipt.co" rel="noopener noreferrer"&gt;AgentReceipt&lt;/a&gt; to solve this problem, including how hash chaining works, why we anchor receipts to a public transparency log, and how three lines of code gives your agent a tamper-proof audit trail.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>What to Tell Claude Code to Test (and What to Skip)</title>
      <dc:creator>Ramon</dc:creator>
      <pubDate>Sun, 15 Mar 2026 21:02:12 +0000</pubDate>
      <link>https://dev.to/ramon_galego/what-to-tell-claude-code-to-test-and-what-to-skip-3foo</link>
      <guid>https://dev.to/ramon_galego/what-to-tell-claude-code-to-test-and-what-to-skip-3foo</guid>
      <description>&lt;p&gt;If you're using Claude Code to build apps, you've probably noticed it loves writing tests. Ask it to build a feature and it'll offer to test it. Ask it to fix a bug and it'll suggest adding coverage. Left to its own devices, it will happily generate hundreds of tests for your application.&lt;/p&gt;

&lt;p&gt;Most of them won't matter.&lt;/p&gt;

&lt;p&gt;Here's the filter I use before writing any test: &lt;strong&gt;if this breaks silently, what happens?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is "nothing for a while" or "a user gets a slightly wrong result," skip it. If the answer is "data gets corrupted," "someone gets charged incorrectly," or "the failure is invisible until it's too late," write the test.&lt;/p&gt;

&lt;p&gt;That's the whole framework. Everything below is just applying it.&lt;/p&gt;

&lt;p&gt;One important caveat: this is a strategy for the build phase. If you're shipping an MVP or a side project and you're the only person working on it, this is the right approach. Once you have a team, long-lived systems, and daily deploys, your testing philosophy needs to evolve. But that's a different post.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Claude Code will test if you don't guide it
&lt;/h2&gt;

&lt;p&gt;Claude Code defaults to thoroughness. It will test that your database queries return data, that your UI components render, that your API routes respond with 200, that your forms submit, that your auth redirects work.&lt;/p&gt;

&lt;p&gt;These tests aren't wrong. They're just low value at the start. A broken UI component is visible the moment you open the browser. A failing form submit takes five seconds of manual testing to catch. You don't need automation for things that announce themselves.&lt;/p&gt;

&lt;p&gt;What you need automation for are the things that fail quietly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The areas worth testing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Business logic with edge cases
&lt;/h3&gt;

&lt;p&gt;Any function that makes a decision based on data is worth testing. Not the happy path, which you'll notice when it breaks. The edge cases are what get you.&lt;/p&gt;

&lt;p&gt;In a monitoring tool I built called &lt;a href="https://pulsemon.dev" rel="noopener noreferrer"&gt;PulseMon&lt;/a&gt;, the core checker function determines whether a monitor is late based on the last ping timestamp, the expected interval, and the grace period. It's about 20 lines of pure logic with 9 tests covering scenarios like: what if the last ping arrived exactly at the deadline, what if there's no ping at all, what if the grace period is zero.&lt;/p&gt;

&lt;p&gt;That function runs every 60 seconds for every user. If it's wrong, monitors either never alert or alert constantly. Neither failure is obvious until users complain. That's the definition of something worth testing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tell Claude Code:&lt;/strong&gt; &lt;em&gt;"Write tests for the core business logic only. Focus on edge cases, not the happy path."&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  External integrations that handle money or critical data
&lt;/h3&gt;

&lt;p&gt;Stripe webhooks. Payment processing. Anything where a bug means someone gets charged twice, doesn't get charged at all, or loses access to something they paid for.&lt;/p&gt;

&lt;p&gt;These are worth testing because the failure modes are severe and the bugs are subtle. A wrong status code, a missing field, an event type you didn't handle. These don't throw obvious errors. They silently do the wrong thing.&lt;/p&gt;

&lt;p&gt;For PulseMon's Stripe webhook handler there are 10 tests covering: subscription created, updated, deleted, payment failed, and an invalid signature. That last one matters because without it, anyone can send fake webhook events to your endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tell Claude Code:&lt;/strong&gt; &lt;em&gt;"Test the Stripe webhook handler. Cover subscription created, updated, deleted, and invalid signature. Mock the Stripe library at the module boundary."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Unit tests cover your logic. Before going live, run one real test with the Stripe CLI to verify the raw body handling and your webhook secret are configured correctly. That catches the two bugs tests can't.&lt;/p&gt;




&lt;h3&gt;
  
  
  Authorisation boundaries
&lt;/h3&gt;

&lt;p&gt;Not "can a user log in" since that's visible immediately if it breaks. The subtle version: can user A access user B's data?&lt;/p&gt;

&lt;p&gt;In any multi-user app, the query that fetches data scoped to the current user is the one worth testing. The bug that leaks one user's data to another is catastrophic and invisible. You won't catch it in manual testing because you're always logged in as the same user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tell Claude Code:&lt;/strong&gt; &lt;em&gt;"Write tests that verify a user cannot access another user's resources. Mock the database and test that all queries include the userId filter."&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Anything that runs on a schedule without human oversight
&lt;/h3&gt;

&lt;p&gt;Cron jobs, background workers, scheduled cleaners. If nobody is watching them run, you need tests for the logic inside. Not integration tests that actually fire the job, but unit tests that cover what the job decides.&lt;/p&gt;

&lt;p&gt;If your cleanup job deletes records older than 30 days, test that it deletes the right records and leaves the wrong ones alone. These jobs run at 3am and nobody checks them.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to tell Claude Code to skip
&lt;/h2&gt;

&lt;h3&gt;
  
  
  UI tests during early development
&lt;/h3&gt;

&lt;p&gt;Skipping UI tests is not a blanket recommendation. It's a prioritisation call for when you're moving fast.&lt;/p&gt;

&lt;p&gt;During the initial build, a broken button is visible the moment you look at the screen. Testing it at that stage slows you down without adding much. But once your core conversion paths exist, things like signup, onboarding, and checkout, they are worth protecting. A global CSS change or a Tailwind config update can silently hide a button on mobile and you won't catch it manually every time.&lt;/p&gt;

&lt;p&gt;The practical version: skip UI tests while you're building, add them for your most critical flows once they're stable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tell Claude Code:&lt;/strong&gt; &lt;em&gt;"Skip UI component tests for now. We'll add coverage for critical conversion paths once they're stable."&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Simple CRUD operations
&lt;/h3&gt;

&lt;p&gt;Creating, reading, updating, and deleting records doesn't need tests if it's just calling an ORM method. The ORM is already tested. Your thin wrapper around it doesn't need coverage.&lt;/p&gt;

&lt;p&gt;The exception is CRUD that enforces business rules. A create operation that checks plan limits before inserting is worth testing. A create operation that just calls &lt;code&gt;db.insert()&lt;/code&gt; is not.&lt;/p&gt;




&lt;h3&gt;
  
  
  Auth configuration, but not auth logic
&lt;/h3&gt;

&lt;p&gt;Libraries like Auth.js are tested by their maintainers. Whether your sign-in redirect fires correctly doesn't need coverage. You'll know immediately if it breaks.&lt;/p&gt;

&lt;p&gt;What is worth testing is the authorisation logic you write yourself: middleware that checks roles, functions that decide what a user can see, session handling that scopes data correctly. Those are yours to own, and that's where leaks happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tell Claude Code:&lt;/strong&gt; &lt;em&gt;"Skip testing that the auth redirect fires. Do test any custom middleware, role checks, or session scoping logic we've written."&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  A prompt that actually works
&lt;/h2&gt;

&lt;p&gt;When starting a new feature, give Claude Code this framing before asking it to write tests:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We only write tests for high-stakes areas where a silent failure would cause real damage. This means: core business logic with edge cases, external integrations that handle payments or critical data, and authorisation boundaries that prevent data leaks. Skip UI tests during initial build, basic CRUD, and standard auth library configuration. For each test you write, add a comment at the top of the test explaining in one sentence why a silent bug here would be a serious problem. If you can't write that sentence, the test shouldn't exist."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The comment becomes permanent documentation for why the test was written. Six months from now, when you're wondering whether you can delete it, the answer is right there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the filter matters more than the framework
&lt;/h2&gt;

&lt;p&gt;Coverage targets and exhaustive test suites are a different conversation to the one most solo developers building with AI assistance need to have right now. The more immediate problem is Claude Code generating 80 tests when 15 would have been enough, most of them testing things that would have been obviously broken on first look.&lt;/p&gt;

&lt;p&gt;Start with the filter. Test the things that fail silently and cause real damage. Build coverage from there as your product matures.&lt;/p&gt;

&lt;p&gt;Write tests for those. Skip everything else. Tell Claude Code exactly that and it'll spend its time on the things that actually matter.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>webdev</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>Your Uptime Monitor Says Green. Your Users Disagree.</title>
      <dc:creator>Ramon</dc:creator>
      <pubDate>Thu, 12 Mar 2026 16:35:45 +0000</pubDate>
      <link>https://dev.to/ramon_galego/your-uptime-monitor-says-green-your-users-disagree-b1o</link>
      <guid>https://dev.to/ramon_galego/your-uptime-monitor-says-green-your-users-disagree-b1o</guid>
      <description>&lt;p&gt;Your uptime monitor pings your homepage every 60 seconds and gets a 200 back. Green. Healthy. No alerts.&lt;/p&gt;

&lt;p&gt;Meanwhile, your checkout is broken because a payment webhook stopped processing three hours ago. Your welcome emails are queued and going nowhere. Your nightly sync hasn't run since Tuesday.&lt;/p&gt;

&lt;p&gt;This is the gap nobody talks about: uptime monitoring tells you your server is alive. It does not tell you your app is working.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an uptime check actually does
&lt;/h2&gt;

&lt;p&gt;A traditional uptime check sends an HTTP request to a URL and waits for a response. If it gets one, the monitor turns green. That's it.&lt;/p&gt;

&lt;p&gt;It doesn't know whether your database is accepting writes. It doesn't know whether your background workers are running. It doesn't know whether the queue has 40,000 unprocessed jobs backed up behind a dead consumer.&lt;/p&gt;

&lt;p&gt;A server can respond with 200 OK while being completely broken in every way that matters to your users.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure modes your ping check misses
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Background workers dying quietly&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most apps have workers running alongside the web server — email dispatch, order processing, report generation. These processes don't have a URL.&lt;br&gt;
Nothing pings them. When they crash or get stuck, there's no signal.&lt;/p&gt;

&lt;p&gt;Your web server keeps responding 200. The workers sit dead. Orders pile up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queue backlogs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A queue that's growing is worse than a queue that's empty. Your uptime monitor has no idea your jobs are sitting unprocessed because a consumer crashed. Users submit forms. The forms go into the queue. Nothing comes out the other side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third-party integration failures&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your app might be up but calling an external API that started returning 500s two hours ago. Stripe, Twilio, SendGrid, whatever. Your server is healthy.&lt;br&gt;
Your users' experience is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database write failures&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A read-only replica can serve your homepage and return 200 all day while your primary database rejects writes. Users think the form submitted. It didn't.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix: make your health check do real work
&lt;/h2&gt;

&lt;p&gt;The standard move is to point your uptime monitor at &lt;code&gt;/health&lt;/code&gt; and call it done. That endpoint usually just returns 200. It tells you the process is running, nothing more.&lt;/p&gt;

&lt;p&gt;Make it test something real instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This tells you nothing
&lt;/span&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;health&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# This actually catches problems
&lt;/span&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;health&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;db_ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check_database_write&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;queue_depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_queue_depth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;worker_last_seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_worker_heartbeat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;db_ok&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;queue_depth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;db_ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queue_depth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;queue_depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;worker_last_seen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;worker_last_seen&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;Now when your uptime monitor hits &lt;code&gt;/health&lt;/code&gt;, it's actually probing your database writes, your queue state, and whether your workers are alive. A 503 means something real is broken, not just that the process died.&lt;/p&gt;

&lt;p&gt;The rule of thumb: if a failure in X would affect users, X should be in your health check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring things that don't have a URL
&lt;/h2&gt;

&lt;p&gt;Workers and scheduled tasks are harder because there's nothing to ping. The approach is to flip it — instead of you checking on them, they check in with you.&lt;/p&gt;

&lt;p&gt;At the end of each successful cycle, the worker hits a heartbeat URL. If the heartbeat stops arriving on schedule, you get alerted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_worker&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;process_queue_batch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://pulsemon.dev/api/ping/order-queue-worker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key detail for workers: set the expected interval to at least twice the cycle time. A slow batch shouldn't trigger a false alert.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on alert fatigue
&lt;/h2&gt;

&lt;p&gt;One reason teams skip monitoring background tasks is they've been burned by flaky alerts before. A monitor that fires constantly gets muted. Then it fires for real and nobody notices.&lt;/p&gt;

&lt;p&gt;Heartbeat monitoring sidesteps this because the only alert condition is absence. There's no false positive from a brief network blip or a slow response. Either the job ran and checked in, or it didn't. That binary clarity means alerts stay trustworthy and actually get acted on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to audit in your own stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Any &lt;code&gt;/health&lt;/code&gt; endpoint that just returns 200 without touching the database&lt;/li&gt;
&lt;li&gt;Background workers with no heartbeat&lt;/li&gt;
&lt;li&gt;Queues with no depth monitoring&lt;/li&gt;
&lt;li&gt;Third-party integrations you assume are working because your server is up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it can break without your uptime monitor noticing, it needs a second line of defence.&lt;/p&gt;




&lt;p&gt;PulseMon is heartbeat monitoring for your cron jobs, background workers, and scheduled tasks. Add a single ping to the end of any job and get alerted via email, Slack, Discord, or webhook when it stops running on schedule.&lt;/p&gt;

&lt;p&gt;Free plan has 30 monitors with 2-minute checks if you want to poke around: &lt;a href="https://pulsemon.dev/" rel="noopener noreferrer"&gt;PulseMon.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>devops</category>
      <category>webdev</category>
      <category>backend</category>
    </item>
    <item>
      <title>Why Your Cron Jobs Fail Silently (And How to Fix It)</title>
      <dc:creator>Ramon</dc:creator>
      <pubDate>Fri, 06 Mar 2026 22:37:31 +0000</pubDate>
      <link>https://dev.to/ramon_galego/why-your-cron-jobs-fail-silently-and-how-to-fix-it-164a</link>
      <guid>https://dev.to/ramon_galego/why-your-cron-jobs-fail-silently-and-how-to-fix-it-164a</guid>
      <description>&lt;p&gt;Your database backup runs every night at 2 AM. Your invoice generator fires every Monday morning. Your cache warmer runs every five minutes. They all work great until they don't.&lt;/p&gt;

&lt;p&gt;The problem with cron jobs is that they fail the same way they run: silently. Nobody is watching stdout at 2 AM. There's no browser to show an error page. When a cron job stops working, the only signal is the absence of something happening.&lt;/p&gt;

&lt;p&gt;You find out on a Friday afternoon that backups haven't run since Tuesday. Or a customer emails you because their weekly report never arrived. Or your disk fills up because the cleanup job died three weeks ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why cron jobs fail
&lt;/h2&gt;

&lt;p&gt;The cron daemon itself is reliable. It has been running scheduled tasks on Unix systems since 1979. The daemon is not the problem. Everything around it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server reboots.&lt;/strong&gt; After a reboot, cron usually starts back up. But if your job depends on a mounted volume, a running database, or a network connection that takes 30 seconds to initialize, the first run after reboot fails. Silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disk full.&lt;/strong&gt; Your job tries to write a temp file or a log entry. It can't. It crashes. Cron doesn't care.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependency failures.&lt;/strong&gt; The API you're calling is down. The database connection times out. The S3 bucket policy changed. Your job throws an exception on line 12 and exits with code 1. Nobody notices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timezone issues.&lt;/strong&gt; You deployed to a server in UTC but wrote your cron expression assuming US Eastern. The job runs at the wrong time, or during a DST transition, it runs twice. Or zero times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The job itself crashes before logging.&lt;/strong&gt; This is the worst one. If your error handling depends on the job running long enough to reach the catch block, an early segfault or OOM kill means zero evidence that anything went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why traditional monitoring misses this
&lt;/h2&gt;

&lt;p&gt;Most monitoring tools watch for things that are happening: high CPU, slow responses, error rate spikes. They're good at detecting active failures.&lt;/p&gt;

&lt;p&gt;Cron job failures are passive. They're the absence of something happening. Your APM won't alert you that a script didn't run. Your error tracker can't capture an exception from a process that never started.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dead man's switch pattern
&lt;/h2&gt;

&lt;p&gt;The fix is to flip the model. Instead of watching for failure, watch for the absence of success.&lt;/p&gt;

&lt;p&gt;This is called a dead man's switch, or heartbeat monitoring. The idea is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a monitor with an expected interval (say, "every 24 hours")&lt;/li&gt;
&lt;li&gt;Add a ping to the end of your job&lt;/li&gt;
&lt;li&gt;If the ping doesn't arrive on time, you get alerted&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key insight: you're not monitoring whether the job failed. You're monitoring whether it succeeded. If you don't hear from it, something went wrong. You don't need to know what.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting it up
&lt;/h2&gt;

&lt;p&gt;Add a single HTTP request to the end of your script. If the script completes successfully, the ping fires. If it crashes, hangs, or never starts, the ping never arrives and you get an alert.&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# backup-database.sh&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;  &lt;span class="c"&gt;# Exit on any error&lt;/span&gt;

pg_dump &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/backup.sql.gz
aws s3 &lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/backup.sql.gz s3://my-backups/&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y-%m-%d&lt;span class="si"&gt;)&lt;/span&gt;.sql.gz
&lt;span class="nb"&gt;rm&lt;/span&gt; /tmp/backup.sql.gz

&lt;span class="c"&gt;# Report success&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="nt"&gt;--retry&lt;/span&gt; 3 https://pulsemon.dev/api/ping/nightly-backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;set -e&lt;/code&gt; flag means the script exits on any error. The &lt;code&gt;curl&lt;/code&gt; at the end only runs if everything above it succeeded. If &lt;code&gt;pg_dump&lt;/code&gt; fails, if S3 upload fails, if the disk is full, the ping never fires.&lt;/p&gt;

&lt;p&gt;For Python:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# ... your job logic here ...
&lt;/span&gt;    &lt;span class="nf"&gt;run_etl_pipeline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://pulsemon.dev/api/ping/nightly-etl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Node.js:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ... your job logic here ...&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processQueue&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://pulsemon.dev/api/ping/queue-processor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What you should monitor
&lt;/h2&gt;

&lt;p&gt;Any scheduled process that runs unattended:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database backups&lt;/strong&gt; are the most common silent failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email queues&lt;/strong&gt; stop processing and nobody complains for days because they assume it's normal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data syncs&lt;/strong&gt; between services. Your analytics dashboard shows stale numbers but looks fine at a glance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certificate renewals&lt;/strong&gt; from Let's Encrypt. The cert expires and your site shows a scary browser warning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleanup jobs&lt;/strong&gt; that free disk space. When they stop, other services start crashing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these run on your infrastructure, they should have a heartbeat monitor. It takes less time to set up than it does to recover from the failure.&lt;/p&gt;

&lt;p&gt;I built PulseMon to solve this for my own projects. Free tier with 30 monitors if you want to try it: &lt;a href="https://pulsemon.dev/" rel="noopener noreferrer"&gt;PulseMon.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>monitoring</category>
      <category>webdev</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
