<?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: Mike Tickstem</title>
    <description>The latest articles on DEV Community by Mike Tickstem (@mike_tickstem).</description>
    <link>https://dev.to/mike_tickstem</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%2F3894790%2Fdede0ddc-2424-45ae-b9aa-b3f21fa08300.png</url>
      <title>DEV Community: Mike Tickstem</title>
      <link>https://dev.to/mike_tickstem</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mike_tickstem"/>
    <language>en</language>
    <item>
      <title>Vercel cron alternative: what to use when built-in cron isn't enough</title>
      <dc:creator>Mike Tickstem</dc:creator>
      <pubDate>Wed, 03 Jun 2026 12:39:28 +0000</pubDate>
      <link>https://dev.to/mike_tickstem/vercel-cron-alternative-what-to-use-when-built-in-cron-isnt-enough-52dp</link>
      <guid>https://dev.to/mike_tickstem/vercel-cron-alternative-what-to-use-when-built-in-cron-isnt-enough-52dp</guid>
      <description>&lt;p&gt;Vercel's built-in cron triggers your serverless functions on a schedule. For simple use cases it works. But it has no failure alerts, no execution history on the Hobby plan, and no way to know whether your function actually completed successfully — only that it was called.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Vercel cron falls short
&lt;/h2&gt;

&lt;p&gt;Vercel cron works by invoking one of your API routes on a schedule defined in &lt;code&gt;vercel.json&lt;/code&gt;. The invocation is fire-and-forget — if your function times out, throws an error, or returns a non-2xx status, you get no alert. You find out when a user reports something is broken.&lt;/p&gt;

&lt;p&gt;The specific gaps developers run into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No failure alerts.&lt;/strong&gt; Vercel does not send an email or webhook if your scheduled function fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No execution history on Hobby.&lt;/strong&gt; The free plan does not retain cron execution history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout ceiling.&lt;/strong&gt; Functions are subject to the same timeout limits as all serverless functions — 10 seconds on Hobby, up to 300 seconds on Pro.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP-only.&lt;/strong&gt; Vercel cron calls an HTTP endpoint on your app. You cannot schedule arbitrary background work outside your deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No heartbeat monitoring.&lt;/strong&gt; Even if your function is called successfully, you have no built-in way to verify it &lt;em&gt;completed&lt;/em&gt; its work — only that it was invoked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimum 1-hour interval on Hobby.&lt;/strong&gt; Sub-hourly schedules require a paid plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are hitting any of these limitations, you need an external tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Schedules jobs&lt;/th&gt;
&lt;th&gt;Failure alerts&lt;/th&gt;
&lt;th&gt;Heartbeat&lt;/th&gt;
&lt;th&gt;Uptime monitoring&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vercel built-in&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓ (1h min)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tickstem&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upstash QStash&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓ (retries)&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inngest&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cron-job.org&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓ (basic)&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Tickstem — cron + heartbeat + uptime in one API key
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; developers who need scheduling, failure alerts, heartbeat monitoring, and uptime checks without managing multiple tools.&lt;/p&gt;

&lt;p&gt;Tickstem is an external HTTP cron scheduler with built-in monitoring. You register your Vercel endpoint as a cron job, and Tickstem calls it on your schedule — every minute if needed, regardless of your Vercel plan. You get email alerts when a job fails or times out, full execution history, and response assertions to verify your endpoint returned the right status code.&lt;/p&gt;

&lt;p&gt;Where it goes further than a simple cron service: Tickstem includes heartbeat monitoring. Your function pings a unique token URL at the end of each successful run. If the ping stops arriving within the expected window, you get alerted — catching cases where the function was called but silently failed partway through.&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;# Register your Vercel endpoint as a cron job&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.tickstem.dev/v1/jobs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "name": "nightly-sync",
    "schedule": "0 2 * * *",
    "endpoint": "https://your-app.vercel.app/api/nightly-sync"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Free tier: 1,000 executions, 5 uptime monitors, 5 heartbeat monitors per month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upstash QStash — reliable delivery with retries
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; developers already using Upstash who want guaranteed delivery with automatic retries.&lt;/p&gt;

&lt;p&gt;QStash is a message queue and scheduler designed for serverless environments. You publish a message with a delay or schedule, and QStash delivers it to your endpoint with automatic retries on failure. It integrates naturally with the Vercel ecosystem.&lt;/p&gt;

&lt;p&gt;It does not cover heartbeat monitoring or uptime checks — it is a scheduling and delivery layer, not a monitoring layer. Good choice if retry semantics matter more than observability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inngest — complex multi-step background jobs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; multi-step workflows with fan-out, conditional logic, and event-driven triggers.&lt;/p&gt;

&lt;p&gt;Inngest lets you define background functions in code and trigger them on a schedule or in response to events. It handles retries, concurrency, and step functions — things that are difficult to build reliably in a stateless serverless environment. First-class Vercel and Next.js integration.&lt;/p&gt;

&lt;p&gt;The tradeoff is complexity. Inngest requires adding their SDK and restructuring your background work as Inngest functions. For simple "call this endpoint on a schedule and alert me if it fails," it is more than you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  cron-job.org — simple external cron, free
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; plugging the scheduling gap with zero added complexity.&lt;/p&gt;

&lt;p&gt;cron-job.org is a free external cron service that calls your URL on a schedule. Basic email alerts on failure, execution history retained. No SDK, no heartbeat monitoring, no uptime checks.&lt;/p&gt;

&lt;p&gt;For developers who just need the scheduling gap filled without adding any infrastructure complexity, it is the lowest-friction option.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Just need sub-hourly scheduling and basic failure alerts:&lt;/strong&gt; cron-job.org covers the basics for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need scheduling + know when the job actually completed its work:&lt;/strong&gt; Tickstem — heartbeat monitoring is the missing piece Vercel does not provide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need reliable delivery with retries and already use Upstash:&lt;/strong&gt; QStash fits naturally into that stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building complex multi-step background workflows:&lt;/strong&gt; Inngest is built for that use case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need cron + heartbeat + uptime without managing three tools:&lt;/strong&gt; Tickstem bundles all three under one API key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern worth avoiding: starting with Vercel's built-in cron, adding cron-job.org for alerts, and eventually needing heartbeat monitoring — ending up with three separate tools and three places to check when something breaks.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://tickstem.dev/blog/vercel-cron-alternative" rel="noopener noreferrer"&gt;tickstem.dev&lt;/a&gt;. Also worth reading: &lt;a href="https://tickstem.dev/blog/why-cron-jobs-fail-on-serverless" rel="noopener noreferrer"&gt;Why cron jobs fail silently on serverless platforms&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vercel</category>
      <category>serverless</category>
      <category>webdev</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Heartbeat monitoring for AI agent pipelines</title>
      <dc:creator>Mike Tickstem</dc:creator>
      <pubDate>Tue, 26 May 2026 12:59:58 +0000</pubDate>
      <link>https://dev.to/mike_tickstem/heartbeat-monitoring-for-ai-agent-pipelines-5ag8</link>
      <guid>https://dev.to/mike_tickstem/heartbeat-monitoring-for-ai-agent-pipelines-5ag8</guid>
      <description>&lt;p&gt;You deploy an AI agent to run nightly. It summarises data, writes a report, sends a Slack message. You set up uptime monitoring on the endpoint. The monitor stays green. Three days later you notice the Slack messages stopped. The agent hasn't run since Tuesday — and nothing alerted you.&lt;/p&gt;

&lt;p&gt;This is the failure mode heartbeat monitoring is designed to catch. Here's how it works and why it's particularly important for AI agent pipelines.&lt;/p&gt;

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

&lt;p&gt;A dead man's switch alerts when something &lt;em&gt;stops happening&lt;/em&gt;. Traditional monitoring alerts when something starts happening — a server goes down, an error rate spikes, a response time increases.&lt;/p&gt;

&lt;p&gt;For AI agents, the dangerous failure is silence. The agent stops running. No error is thrown. No endpoint goes down. The work just quietly ceases. A dead man's switch catches this by expecting a regular signal — if the signal stops, something is wrong.&lt;/p&gt;

&lt;p&gt;The implementation is straightforward: at the end of every successful agent run, after the real work is done, send a ping to a heartbeat URL. If the ping stops arriving within the expected window, you get an alert.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI agents need this more than traditional jobs
&lt;/h2&gt;

&lt;p&gt;Traditional cron jobs fail loudly — a non-zero exit code, an exception in the logs, a failed database write. You usually know something went wrong.&lt;/p&gt;

&lt;p&gt;AI agents fail quietly. The model might hit a rate limit and return a graceful fallback response. A tool call might silently fail and the agent continues without it. The task might complete but produce empty or corrupted output — and your application code never raises an error because it got a valid HTTP response.&lt;/p&gt;

&lt;p&gt;In all these cases, the endpoint is up, the job "ran," and traditional monitoring sees nothing. The heartbeat sees everything — because the agent itself decides whether to send the ping, and it only pings on genuine success.&lt;/p&gt;

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

&lt;p&gt;Create the heartbeat once and save the token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.tickstem.dev/v1/heartbeats &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name": "nightly-summary-agent", "interval_secs": 86400, "grace_secs": 3600}'&lt;/span&gt;
&lt;span class="c"&gt;# → {"token": "your-64-char-token", ...}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your agent's task handler, ping only after all the real work is verified complete:&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;runNightlySummaryAgent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateSummary&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;summary generation failed or returned empty output&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;postToSlack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;summary&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;writeToDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// only ping after everything succeeded&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="s2"&gt;`https://api.tickstem.dev/v1/heartbeats/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;HEARTBEAT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/ping`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&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="o"&gt;=&amp;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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;heartbeat ping failed:&lt;/span&gt;&lt;span class="dl"&gt;"&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="c1"&gt;// non-fatal&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ping is fire-and-forget — a network error on the ping should never block your agent from returning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting the right interval and grace window
&lt;/h2&gt;

&lt;p&gt;The interval is how often you expect the agent to run. The grace window absorbs variance.&lt;/p&gt;

&lt;p&gt;A practical starting point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hourly agents:&lt;/strong&gt; interval 3600s, grace 600s&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily agents:&lt;/strong&gt; interval 86400s, grace 3600s&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weekly agents:&lt;/strong&gt; interval 604800s, grace 7200s&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After a week of runs, check your actual execution durations and tighten the grace window to 2-3x your p95 runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-step pipelines
&lt;/h2&gt;

&lt;p&gt;For agents that run a pipeline — fetch data, process it, write results, notify downstream — consider a heartbeat per stage if any stage can fail silently. One heartbeat at the end of the full pipeline tells you the pipeline completed. Individual stage heartbeats tell you exactly where it stopped.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A useful rule: the heartbeat ping should only fire after your agent has verified its own output — database write succeeded, Slack message delivered, output passes a sanity check. Not before.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Pausing during deployments
&lt;/h2&gt;

&lt;p&gt;Deployments are the most common source of false heartbeat alerts. Pause before deploying:&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;# before deploy&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.tickstem.dev/v1/heartbeats/&lt;span class="nv"&gt;$HEARTBEAT_ID&lt;/span&gt;/pause &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# after deploy completes&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.tickstem.dev/v1/heartbeats/&lt;span class="nv"&gt;$HEARTBEAT_ID&lt;/span&gt;/resume &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Via MCP
&lt;/h2&gt;

&lt;p&gt;If you're using Claude Code or another MCP-compatible client, the Tickstem MCP server exposes &lt;code&gt;create_heartbeat&lt;/code&gt; and &lt;code&gt;ping_heartbeat&lt;/code&gt; as native tools. The agent can set up its own dead man's switch during the initial scaffolding step.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tickstem provides heartbeat monitoring, uptime checks, cron scheduling, and email verification under one API key. &lt;a href="https://app.tickstem.dev/login" rel="noopener noreferrer"&gt;Free tier at app.tickstem.dev&lt;/a&gt; — no credit card required.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>mcp</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Monitoring AI agents in production</title>
      <dc:creator>Mike Tickstem</dc:creator>
      <pubDate>Tue, 26 May 2026 12:58:00 +0000</pubDate>
      <link>https://dev.to/mike_tickstem/monitoring-ai-agents-in-production-1pae</link>
      <guid>https://dev.to/mike_tickstem/monitoring-ai-agents-in-production-1pae</guid>
      <description>&lt;p&gt;Traditional monitoring asks one question: is the server up? If the endpoint returns 200, everything is fine. AI agents break that assumption. The server can be perfectly healthy while the agent silently produces wrong outputs, skips steps, runs over budget, or stops working entirely — all without triggering a single alert.&lt;/p&gt;

&lt;p&gt;Monitoring autonomous agents requires a different mental model. Here's what actually breaks and how to catch it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What traditional monitoring misses
&lt;/h2&gt;

&lt;p&gt;Uptime monitoring tells you the endpoint responded. It says nothing about what the agent did inside that response. An agent endpoint that returns &lt;code&gt;{"status": "ok"}&lt;/code&gt; in 50ms might have skipped the entire task due to a context length limit, a rate limit on the model API, or a malformed tool call that silently failed.&lt;/p&gt;

&lt;p&gt;The failure modes specific to AI agents in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Silent tool failures.&lt;/strong&gt; A tool call returns an error that the model handles by continuing without it. The task "completes" but with missing data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context window exhaustion.&lt;/strong&gt; Long-running agents hit token limits mid-task and truncate their work. The HTTP response is still 200.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model API degradation.&lt;/strong&gt; The underlying model API is slow or returning degraded outputs. Your endpoint is up; the work is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drift over time.&lt;/strong&gt; An agent that worked last week starts producing subtly different outputs as the model is updated. No alert fires — outputs just quietly change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled run skips.&lt;/strong&gt; The agent was supposed to run at 06:00. It didn't. Nothing in your existing monitoring catches this because the server never went down.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The three layers of agent monitoring
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Layer 1: Uptime monitoring
&lt;/h2&gt;

&lt;p&gt;Still necessary — just not sufficient. Your agent's HTTP endpoint should be monitored for availability and response time. A degraded model API often manifests first as increased latency before it causes failures.&lt;/p&gt;

&lt;p&gt;Set up an uptime monitor on the endpoint your agent exposes. A 30-second check interval catches most outages before users do. Configure timeout alerts — if your agent normally responds in under 10 seconds and starts taking 90, something is wrong even if it's still returning 200.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.tickstem.dev/v1/monitors &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name": "summary-agent-endpoint", "url": "https://your-app.com/agents/summary/health", "interval_secs": 30, "timeout_secs": 15}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Layer 2: Heartbeat monitoring
&lt;/h2&gt;

&lt;p&gt;Uptime tells you the server is alive. Heartbeat tells you the agent actually did the work.&lt;/p&gt;

&lt;p&gt;A heartbeat monitor works as a dead man's switch: your agent sends a ping after each successful completion. If the ping stops arriving within the expected window, you get an alert. The server being up is irrelevant — if the work stopped happening, the heartbeat catches 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="c"&gt;# Create a heartbeat — save the token&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.tickstem.dev/v1/heartbeats &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"daily-summary-agent","interval_secs":86400,"grace_secs":3600}'&lt;/span&gt;

&lt;span class="c"&gt;# At the end of every successful agent run&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.tickstem.dev/v1/heartbeats/&lt;span class="nv"&gt;$HEARTBEAT_TOKEN&lt;/span&gt;/ping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ping only fires on success — after the agent has verified its own output. Silence means failure, regardless of what the HTTP response said.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: Execution history
&lt;/h2&gt;

&lt;p&gt;The most underused layer. Every scheduled agent run should produce a logged record: when it ran, how long it took, whether it succeeded, and what it returned.&lt;/p&gt;

&lt;p&gt;Without this, debugging a failure means reconstructing what happened from scattered logs. With it, you open the execution history and see immediately: the run at 06:03 took 4 minutes instead of the usual 45 seconds, returned a 500, and the response body contains a rate limit error from the model API.&lt;/p&gt;

&lt;p&gt;If you're using &lt;a href="https://tickstem.dev/blog/scheduling-tasks-ai-agent-workflows" rel="noopener noreferrer"&gt;HTTP-based scheduling&lt;/a&gt; for your agent, execution history comes for free — every run is logged with the full request and response.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A practical rule: any agent task that runs on a schedule and produces output that other systems depend on needs all three layers. Uptime alone is not monitoring — it's a pulse check.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Wiring it up via MCP
&lt;/h2&gt;

&lt;p&gt;If you're building with Claude Code or a similar MCP-compatible agent, you can set up the full monitoring stack from within your editor. The Tickstem MCP server exposes &lt;code&gt;create_monitor&lt;/code&gt;, &lt;code&gt;create_heartbeat&lt;/code&gt;, and &lt;code&gt;list_executions&lt;/code&gt; as native tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  What good agent monitoring looks like
&lt;/h2&gt;

&lt;p&gt;The goal is to answer three questions at any point in time, without digging through logs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the agent endpoint reachable and responding normally? (uptime)&lt;/li&gt;
&lt;li&gt;Did the agent complete its last scheduled task? (heartbeat)&lt;/li&gt;
&lt;li&gt;What happened on the last N runs? (execution history)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When all three are in place, debugging shifts from "something might be wrong, let me check everything" to "here's exactly what happened and when."&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tickstem provides uptime monitoring, heartbeat checks, cron scheduling, and email verification under one API key. &lt;a href="https://app.tickstem.dev/login" rel="noopener noreferrer"&gt;Free tier at app.tickstem.dev&lt;/a&gt; — no credit card required.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>monitoring</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Scheduling recurring tasks in AI agent workflows</title>
      <dc:creator>Mike Tickstem</dc:creator>
      <pubDate>Tue, 26 May 2026 12:47:17 +0000</pubDate>
      <link>https://dev.to/mike_tickstem/scheduling-recurring-tasks-in-ai-agent-workflows-3pf7</link>
      <guid>https://dev.to/mike_tickstem/scheduling-recurring-tasks-in-ai-agent-workflows-3pf7</guid>
      <description>&lt;p&gt;When you build an AI agent that does something useful — summarises documents, monitors a feed, sends a report, syncs data — you eventually hit the same question: how do I make it run on a schedule? Not once, triggered manually. On a schedule, reliably, while I'm not watching.&lt;/p&gt;

&lt;p&gt;The answer sounds obvious: cron job. But cron jobs for AI agents have a specific set of failure modes that traditional scheduled tasks don't. This post walks through the problem and how to solve it cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI agent scheduling is different
&lt;/h2&gt;

&lt;p&gt;A traditional cron job runs a deterministic script. It succeeds or it fails. You can usually tell which from the exit code or the output.&lt;/p&gt;

&lt;p&gt;An AI agent run is different in three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Variable duration.&lt;/strong&gt; The same agent task might take 8 seconds or 4 minutes depending on the model, the input, and whether any tools needed retries. A fixed timeout either kills legitimate runs or hides runaway ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial success.&lt;/strong&gt; The agent might complete three out of four steps before failing silently on the fourth. The job exits 0 but the work wasn't done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downstream dependencies.&lt;/strong&gt; Other systems may be waiting on the agent's output. If the agent run is delayed or skipped, nothing downstream fires — and nothing alerts you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These failure modes make it worth thinking carefully about how you schedule agents, not just whether you schedule them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scheduling options
&lt;/h2&gt;

&lt;p&gt;There are three common approaches, each with different tradeoffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: Platform-native cron (Vercel, Railway, Render)
&lt;/h2&gt;

&lt;p&gt;If your agent runs as a serverless function or a hosted service, the platform usually offers built-in cron scheduling. You configure a schedule, the platform sends an HTTP request to your endpoint at the right time.&lt;/p&gt;

&lt;p&gt;This works until it doesn't. Platform cron has no execution history, no alerting on failure, and no guarantee of exactly-once delivery. If the platform retries the request — which it will, eventually — your agent runs twice. For idempotent tasks that's annoying. For tasks that send emails or process payments, it's a bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: A queue with a scheduler
&lt;/h2&gt;

&lt;p&gt;A more robust approach: a dedicated scheduler posts a message to a queue at the scheduled time. A worker pulls from the queue and runs the agent. Whoever pulls the message first wins — the message is gone, so no second worker can pick it up.&lt;/p&gt;

&lt;p&gt;This gives you exactly-once semantics via atomic queue operations. Redis &lt;code&gt;BRPOP&lt;/code&gt; is the classic implementation: it blocks until a message arrives, then removes it and returns it to exactly one caller. Ten workers can be waiting — only one gets the job.&lt;/p&gt;

&lt;p&gt;The tradeoff is operational overhead: you need a queue, a scheduler, and workers, all running and monitored. For a single agent this is usually overkill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: HTTP-based scheduling with execution history
&lt;/h2&gt;

&lt;p&gt;The approach that works well for most agent deployments: an external scheduler sends an HTTP POST to your agent's endpoint on the schedule you define. The scheduler logs every execution — request headers, response status, response body, duration, whether it ran on time.&lt;/p&gt;

&lt;p&gt;When the run fails, you have the full record: what was sent, what came back, how long it took. When runs stop happening, you get an alert. This is what Tickstem provides.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.tickstem.dev/v1/jobs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name": "daily-summary-agent", "url": "https://your-app.com/agents/daily-summary", "schedule": "0 8 * * *", "timeout_secs": 300}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scheduler fires a POST to your endpoint at 08:00 every day. Your endpoint runs the agent and returns when it's done. The execution — status code, response body, duration — is recorded and available in the dashboard or via API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it up with the MCP server
&lt;/h2&gt;

&lt;p&gt;If you're building with Claude Code or another MCP-compatible agent, Tickstem exposes its scheduling tools natively via MCP. The agent can register its own cron jobs without leaving the editor:&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;"mcpServers"&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="nl"&gt;"tickstem"&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="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsk-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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="nl"&gt;"TICKSTEM_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-key"&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="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="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;With the MCP server running, the agent can call &lt;code&gt;create_job&lt;/code&gt;, &lt;code&gt;list_jobs&lt;/code&gt;, and &lt;code&gt;get_executions&lt;/code&gt; as native tools. No context switching, no separate dashboard visit to wire up the schedule.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The MCP integration means the agent can also inspect its own execution history. If a previous run failed, the agent sees that on the next invocation and can adjust — retry a step, notify a human, or log a more specific error.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Handling variable agent duration
&lt;/h2&gt;

&lt;p&gt;Set &lt;code&gt;timeout_secs&lt;/code&gt; generously. For an agent that usually takes 30 seconds but occasionally takes 3 minutes on heavy inputs, a 60-second timeout generates false failures. Start at 5-10x your typical duration, then tighten once you have execution history data to work from.&lt;/p&gt;

&lt;p&gt;Tickstem records the actual duration of every run. After a week of executions, you'll have a realistic p99 — use that as your timeout baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pairing scheduling with heartbeat monitoring
&lt;/h2&gt;

&lt;p&gt;Scheduling tells you when the agent starts. It doesn't tell you whether the agent actually completed the work it was supposed to do.&lt;/p&gt;

&lt;p&gt;For that, add a heartbeat ping at the end of the agent's task — after all the real work is finished, not before. If the ping stops arriving, something went wrong inside the run even though the HTTP response was 200.&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;# At the end of your agent's task handler&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.tickstem.dev/v1/heartbeats/&lt;span class="nv"&gt;$HEARTBEAT_TOKEN&lt;/span&gt;/ping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scheduling + heartbeat together give you the full picture: the scheduler confirms the agent was invoked on time, the heartbeat confirms it did what it was supposed to do. See &lt;a href="https://tickstem.dev/blog/heartbeat-monitoring-for-background-jobs" rel="noopener noreferrer"&gt;heartbeat monitoring for background jobs&lt;/a&gt; for the full pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on exactly-once for agents
&lt;/h2&gt;

&lt;p&gt;Most agent tasks are not naturally idempotent. Sending a summary email twice, creating a duplicate report, or processing the same input twice are real problems. If you're using platform-native cron, add an idempotency check in your endpoint — a database flag, a Redis key, or a content hash — before the agent does anything irreversible.&lt;/p&gt;

&lt;p&gt;If you're using Tickstem's scheduler, the queue-backed execution model handles this at the infrastructure level. One schedule tick, one message, one execution.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tickstem provides HTTP-based cron scheduling, uptime monitoring, heartbeat monitoring, and email verification under one API key. &lt;a href="https://app.tickstem.dev/login" rel="noopener noreferrer"&gt;Free tier at app.tickstem.dev&lt;/a&gt; — no credit card required.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>mcp</category>
      <category>devtools</category>
    </item>
    <item>
      <title>How to detect and block temporary email addresses at signup</title>
      <dc:creator>Mike Tickstem</dc:creator>
      <pubDate>Thu, 21 May 2026 16:43:22 +0000</pubDate>
      <link>https://dev.to/mike_tickstem/how-to-detect-and-block-temporary-email-addresses-at-signup-6pa</link>
      <guid>https://dev.to/mike_tickstem/how-to-detect-and-block-temporary-email-addresses-at-signup-6pa</guid>
      <description>&lt;p&gt;Temp mail services give anyone a working inbox with no registration. They pass every syntax check, often pass MX validation, and vanish within hours. Here's how to reliably detect and block them before they reach your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are temporary email addresses?
&lt;/h2&gt;

&lt;p&gt;Temporary email — also called disposable email, throwaway email, or temp mail — is a short-lived inbox provided by services like Temp-Mail.org, Guerrilla Mail, 10 Minute Mail, Mailinator, and hundreds of others. The user gets a working address instantly, no registration required. Emails arrive in real time. The inbox expires anywhere from 10 minutes to a few days.&lt;/p&gt;

&lt;p&gt;From a purely technical standpoint, these addresses are valid. The domain has MX records. The inbox receives email. A regex check passes. A basic MX lookup passes. The address looks identical to a legitimate one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why temp mail hurts your product
&lt;/h2&gt;

&lt;p&gt;Developers often underestimate the problem until they look at their user table. Common patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free tier abuse.&lt;/strong&gt; A single person creates dozens of accounts to get repeated access to a free trial or free quota.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skewed analytics.&lt;/strong&gt; Signup numbers look healthy but most accounts are one-time throwaways that never convert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead email lists.&lt;/strong&gt; Marketing emails bounce at a high rate once the temp addresses expire, damaging your sender reputation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fraud and spam.&lt;/strong&gt; Bad actors use disposable addresses to post spam, file fake support tickets, or exploit referral bonuses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wasted onboarding cost.&lt;/strong&gt; Every automated welcome email, verification email, and drip sequence fired at a dead address costs money and deliverability reputation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The challenge: temp mail evolves faster than blocklists
&lt;/h2&gt;

&lt;p&gt;The naive solution is a domain blocklist — a text file of known disposable providers. That list exists. You can find it on GitHub with 100,000+ domains. The problem is the list is always stale. New temp mail services appear daily, and existing ones cycle through domains to evade filters.&lt;/p&gt;

&lt;p&gt;A purely blocklist-based approach catches the obvious cases (mailinator.com, guerrillamail.com) but misses the long tail. The detection problem is fundamentally an ongoing one, not a one-time list import.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to detect temporary email addresses
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. MX record checks
&lt;/h3&gt;

&lt;p&gt;Most temp mail providers share infrastructure. Certain mail servers handle thousands of disposable domains. Checking whether a domain's MX record points to a known temporary mail provider's servers catches a large slice of the problem, including domains that don't appear on any blocklist yet.&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;# DNS lookup for MX records&lt;/span&gt;
dig MX tempinbox.com
&lt;span class="c"&gt;# → points to a shared disposable mail server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The limitation: large providers like Gmail and Outlook also run shared infrastructure, so you can't block on shared servers alone. You need to combine MX analysis with domain reputation data.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Domain blocklist lookup
&lt;/h3&gt;

&lt;p&gt;A curated blocklist handles the well-known providers reliably. The key is using a maintained list, not a static file you imported once. Providers like Tickstem update the blocklist continuously, so you get coverage for new domains without any work on your end.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Domain age and registration signals
&lt;/h3&gt;

&lt;p&gt;Temp mail domains are often newly registered or have minimal web presence. WHOIS data and domain age can supplement other signals. A domain registered two days ago with an MX record pointing to shared mail infrastructure is almost certainly disposable.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Pattern matching on common structures
&lt;/h3&gt;

&lt;p&gt;Many temp mail services use predictable address formats: &lt;code&gt;random@domain.com&lt;/code&gt; where the local part is a UUID or random string, or a username that looks machine-generated. Pattern matching alone has too many false positives to be reliable, but it adds signal when combined with other checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. API-based verification (the reliable approach)
&lt;/h3&gt;

&lt;p&gt;All of the above signals need to be combined and weighted. The practical approach is to use a purpose-built verification API that handles the signal aggregation, keeps blocklists current, and returns a single verdict per email address.&lt;/p&gt;

&lt;p&gt;Here's what a one-call check looks like with Tickstem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.tickstem.dev/v1/verify &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer tsk_your_key"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email": "user@tempinbox.com"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user@tempinbox.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"disposable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mx"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"disposable_domain"&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;The &lt;code&gt;disposable&lt;/code&gt; field is the one to act on. Block or flag any address where it returns &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Block at the point of entry
&lt;/h3&gt;

&lt;p&gt;The right place to check is on your signup form, before writing anything to your database. Call the verification API on form submission. If &lt;code&gt;disposable: true&lt;/code&gt;, return an error to the user immediately:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;VerifyClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tickstem/verify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VerifyClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TICKSTEM_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSignup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disposable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please use a permanent email address to sign up.&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;That email address does not appear to be valid.&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="c1"&gt;// proceed with signup&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Soft-block vs hard-block
&lt;/h3&gt;

&lt;p&gt;For most products, a hard block (reject the signup entirely) is the right default. Some products prefer a soft block: allow signup but flag the account for manual review, restrict access to paid features, or require additional verification before granting full access.&lt;/p&gt;

&lt;p&gt;The choice depends on your tolerance for friction. Hard blocks eliminate the problem entirely; soft blocks let you study edge cases before committing to a policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Don't check on every request
&lt;/h3&gt;

&lt;p&gt;Email verification is a signup-time check, not a per-request check. Cache results by domain if you're processing bulk imports. For real-time signup forms, one check per submission is the right pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Popular temp mail services your users are using
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Temp-Mail.org&lt;/td&gt;
&lt;td&gt;Very popular, cycles through many domains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guerrilla Mail&lt;/td&gt;
&lt;td&gt;Long-running, multiple domains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 Minute Mail&lt;/td&gt;
&lt;td&gt;Fixed 10-minute expiry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mailinator&lt;/td&gt;
&lt;td&gt;Public inboxes at mailinator.com and aliases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throwam&lt;/td&gt;
&lt;td&gt;Newer, used to evade basic blocklists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dispostable&lt;/td&gt;
&lt;td&gt;API-accessible disposable inboxes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yopmail&lt;/td&gt;
&lt;td&gt;Persistent but anonymous inboxes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;The list above is illustrative, not exhaustive. There are thousands of active disposable providers. Maintaining your own blocklist is a full-time job — this is why API-based verification is the practical choice for most teams.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What about legitimate use cases?
&lt;/h2&gt;

&lt;p&gt;Privacy-conscious users sometimes use alias services — Apple's Hide My Email, SimpleLogin, or Fastmail aliases — to protect their real address. These are different from temp mail: the address is permanent, forwards to a real inbox, and the user intends to receive ongoing emails.&lt;/p&gt;

&lt;p&gt;A good verification API distinguishes between disposable temp mail and privacy aliases. Tickstem's &lt;code&gt;disposable&lt;/code&gt; flag is false for forwarding aliases because they exhibit the behavior of real, permanent addresses (active MX, established domain, no expiry).&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Blocking temp mail reliably requires combining multiple signals: domain blocklists, MX record analysis, domain reputation, and pattern matching. Doing this yourself means maintaining a continuously updated dataset. A verification API handles that maintenance and gives you a single field to act on.&lt;/p&gt;

&lt;p&gt;The check takes one API call at signup time. The cost of not checking is fake accounts, abused free tiers, and degraded sender reputation — costs that compound over time.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Block temp mail in one API call:&lt;/strong&gt; &lt;a href="https://tickstem.dev/verify" rel="noopener noreferrer"&gt;tickstem.dev&lt;/a&gt; — 500 checks free per month, no credit card required.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>node</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Add production monitoring to Claude Code apps in minutes</title>
      <dc:creator>Mike Tickstem</dc:creator>
      <pubDate>Thu, 21 May 2026 16:43:06 +0000</pubDate>
      <link>https://dev.to/mike_tickstem/add-production-monitoring-to-claude-code-apps-in-minutes-5ck</link>
      <guid>https://dev.to/mike_tickstem/add-production-monitoring-to-claude-code-apps-in-minutes-5ck</guid>
      <description>&lt;p&gt;AI coding agents are getting good at writing application code. The gap is the operational layer — the cron jobs, health checks, and monitoring that keep a production app running. Tickstem's MCP server closes that gap by letting Claude Code provision infrastructure directly, without you switching context to another dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with AI-built apps in production
&lt;/h2&gt;

&lt;p&gt;You build a feature with Claude Code in an afternoon. The code is clean, the tests pass, it deploys. Then the background job that sends daily digests silently stops running. Nobody notices for three days because there is no monitoring. The job was scaffolded by the agent, but the operational layer — the heartbeat check, the cron schedule, the alert on failure — was never set up.&lt;/p&gt;

&lt;p&gt;This is the gap. AI agents are excellent at writing application logic. They are bad at the surrounding infrastructure, not because they lack the ability, but because the tools for provisioning that infrastructure are not natively available to them. They can generate curl commands and tell you what to run — but they cannot actually register the monitor.&lt;/p&gt;

&lt;p&gt;Tickstem's MCP server changes this. Once it is configured, Claude Code can register cron jobs, create uptime monitors, set up heartbeat checks, and verify email addresses as part of the normal development workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing the Tickstem MCP server
&lt;/h2&gt;

&lt;p&gt;First, get a Tickstem API key from &lt;a href="https://app.tickstem.dev/dashboard/keys" rel="noopener noreferrer"&gt;app.tickstem.dev/dashboard/keys&lt;/a&gt;. The free tier is enough to follow this walkthrough.&lt;/p&gt;

&lt;p&gt;Download the &lt;code&gt;tsk-mcp&lt;/code&gt; binary for your platform from &lt;a href="https://github.com/tickstem/mcp/releases" rel="noopener noreferrer"&gt;github.com/tickstem/mcp/releases&lt;/a&gt; and place it somewhere on your &lt;code&gt;PATH&lt;/code&gt;. Then add Tickstem to your Claude Code MCP configuration:&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;# Add to your Claude Code project via CLI&lt;/span&gt;
claude mcp add tickstem tsk-mcp &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="nv"&gt;TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tsk_your_key_here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or add it manually to &lt;code&gt;.claude/settings.json&lt;/code&gt; in your project:&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;"mcpServers"&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="nl"&gt;"tickstem"&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="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsk-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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="nl"&gt;"TICKSTEM_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsk_your_key_here"&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="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="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;Restart Claude Code. When the MCP server connects successfully, Claude has access to Tickstem's tools and can use them without any further instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registering a cron job
&lt;/h2&gt;

&lt;p&gt;Suppose you have just built a daily digest endpoint at &lt;code&gt;/api/cron/digest&lt;/code&gt;. Instead of manually logging into a dashboard to schedule it, you ask Claude directly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You:&lt;/strong&gt; Register a cron job that hits POST /api/cron/digest on my production app every day at 9am UTC, Monday through Friday.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'll register that with Tickstem now.&lt;/p&gt;

&lt;p&gt;✓ Created cron job &lt;strong&gt;daily-digest&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schedule: &lt;code&gt;0 9 * * 1-5&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Endpoint: &lt;code&gt;https://yourapp.com/api/cron/digest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Method: POST&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The job is active. You'll receive an email alert if it fails. You can view execution history at app.tickstem.dev/dashboard/jobs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The job is now registered and running. No dashboard visit, no copy-pasting a webhook URL, no configuring retry logic — that is all handled by Tickstem's scheduler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up a heartbeat monitor
&lt;/h2&gt;

&lt;p&gt;A cron job tells Tickstem when to run your endpoint. A heartbeat monitor is the reverse: your job tells Tickstem it ran successfully. If the ping stops arriving, you get an alert. This catches a different failure mode — the scheduler fires but the job itself crashes or times out silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You:&lt;/strong&gt; Add a heartbeat monitor for the digest job. It should alert me if no ping arrives within 25 hours — that gives a 1-hour buffer on the daily schedule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Created heartbeat monitor &lt;strong&gt;daily-digest-heartbeat&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Interval: 86400s (24h)&lt;/li&gt;
&lt;li&gt;Grace: 3600s (1h)&lt;/li&gt;
&lt;li&gt;Ping URL: &lt;code&gt;https://api.tickstem.dev/v1/heartbeats/a3f8...c2d1/ping&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'll add the ping call to your digest handler. At the end of a successful run, your job should POST to that URL — no auth header needed, the token in the URL is the credential.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude then updates your handler to include the ping:&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;httpx&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_digest&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# ... your existing digest logic ...
&lt;/span&gt;
    &lt;span class="c1"&gt;# Ping Tickstem at the end of a successful run
&lt;/span&gt;    &lt;span class="c1"&gt;# Non-fatal — don't let a monitoring outage block your job
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&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://api.tickstem.dev/v1/heartbeats/a3f8...c2d1/ping&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;5&lt;/span&gt;&lt;span class="p"&gt;,&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="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# log this, but don't raise
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two monitors for one job — the scheduler fires it, the heartbeat confirms it completed. If either fails, you know immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying email addresses at signup
&lt;/h2&gt;

&lt;p&gt;Email verification is the same pattern. You are adding a signup form and want to reject disposable addresses before they reach your database:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You:&lt;/strong&gt; Add email verification to the signup handler. Reject disposable and role-based addresses with a clear error message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'll add Tickstem email verification to your signup handler. The free tier gives you 500 verifications per month — enough for early-stage signups.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tickstem&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;VerifyClient&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;verify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VerifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;signup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;valid&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Email address not accepted: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;disposable&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Disposable email addresses are not accepted.&lt;/span&gt;&lt;span class="sh"&gt;"&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role_based&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Please use a personal email address.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# safe to store
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this looks like end to end
&lt;/h2&gt;

&lt;p&gt;In a typical Claude Code session building a production feature, the full ops setup now takes one conversation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the feature and deploy the endpoint&lt;/li&gt;
&lt;li&gt;Ask Claude to register the cron schedule&lt;/li&gt;
&lt;li&gt;Ask Claude to add a heartbeat monitor and update the handler&lt;/li&gt;
&lt;li&gt;Ask Claude to add email verification to any signup flows&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent provisions everything, writes the integration code, and confirms what was created. You end up with a production-ready feature — scheduler, monitoring, and validation — without touching a separate dashboard.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What Claude Code can do via MCP:&lt;/strong&gt; register and manage cron jobs, verify email addresses, list existing jobs and their execution history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What requires direct API calls (code Claude generates for you):&lt;/strong&gt; heartbeat ping calls, uptime monitor creation, heartbeat monitor creation. Claude writes the integration code; you run it once to provision.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why this matters beyond convenience
&lt;/h2&gt;

&lt;p&gt;The practical benefit is obvious — fewer context switches, faster setup. But there is a structural argument too.&lt;/p&gt;

&lt;p&gt;The operational layer of most applications is set up manually, inconsistently, and often incompletely. Developers know they should add a heartbeat monitor. They do not, because it means opening another tab, finding the dashboard, creating the resource, copying the URL, and updating the code. It takes twenty minutes and breaks flow.&lt;/p&gt;

&lt;p&gt;When that setup is a two-sentence conversation with the agent already open in your editor, it gets done. Every time. The gap between "application code" and "production-ready application" closes because the tooling for both lives in the same place.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Get your API key and add Tickstem to Claude Code:&lt;/strong&gt; &lt;a href="https://app.tickstem.dev/login" rel="noopener noreferrer"&gt;app.tickstem.dev&lt;/a&gt; — free tier includes 1,000 cron executions, 5 heartbeats, 5 uptime monitors, and 500 email verifications per month.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>mcp</category>
      <category>devops</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Healthchecks.io alternatives for developers (2026)</title>
      <dc:creator>Mike Tickstem</dc:creator>
      <pubDate>Wed, 20 May 2026 11:33:29 +0000</pubDate>
      <link>https://dev.to/mike_tickstem/healthchecksio-alternatives-for-developers-2026-bj5</link>
      <guid>https://dev.to/mike_tickstem/healthchecksio-alternatives-for-developers-2026-bj5</guid>
      <description>&lt;p&gt;Healthchecks.io is a well-designed tool for one specific problem: alerting you when a scheduled job stops checking in. If that is all you need, it is hard to beat. But most developers eventually need more — cron scheduling, uptime monitoring for HTTP endpoints, or all three in one place. Here is a clear comparison of the best alternatives.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Healthchecks.io does well
&lt;/h2&gt;

&lt;p&gt;Healthchecks.io is a dead man's switch for cron jobs. Your job sends an HTTP ping to a unique URL after each successful run. If the ping stops arriving within the expected window, Healthchecks.io sends an alert. The concept is simple, the implementation is clean, and it is &lt;a href="https://github.com/healthchecks/healthchecks" rel="noopener noreferrer"&gt;open source&lt;/a&gt; — you can self-host it for free.&lt;/p&gt;

&lt;p&gt;The free hosted tier gives you 20 checks and three months of log history, which is enough for most small projects. Setup takes about five minutes. If you only need heartbeat monitoring and nothing else, it remains an excellent choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why developers look for alternatives
&lt;/h2&gt;

&lt;p&gt;The typical reason is scope. Healthchecks.io monitors whether your job runs — it does not schedule the job, and it does not monitor whether your HTTP endpoints are up. Once you add uptime monitoring for your API and a cron scheduler for your background tasks, you have three separate tools, three dashboards, and three invoices.&lt;/p&gt;

&lt;p&gt;Other common reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosting overhead&lt;/strong&gt;. The open source version requires a server, a Postgres database, and ongoing maintenance. For solo developers this is often not worth the operational cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing at scale&lt;/strong&gt;. The hosted Business plan jumps significantly once you need more than 20 checks or additional team members.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cron scheduling&lt;/strong&gt;. Healthchecks.io tells you a job did not run — it does not run the job for you. Serverless environments like Vercel and Railway make this a common pain point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No API-first workflow&lt;/strong&gt;. Creating and managing checks programmatically is possible but limited compared to tools built around an API-first model.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comparison at a glance
&lt;/h2&gt;

&lt;p&gt;Pricing approximate — verify on each vendor's site before purchasing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Tool&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Heartbeats&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Cron scheduling&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Uptime monitoring&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Free tier&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Starting price&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Healthchecks.io&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;20 checks&lt;/td&gt;
&lt;td&gt;~$20/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tickstem&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;5 heartbeats&lt;/td&gt;
&lt;td&gt;$12/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cronitor&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;5 monitors&lt;/td&gt;
&lt;td&gt;~$25/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dead Man's Snitch&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;1 snitch&lt;/td&gt;
&lt;td&gt;~$9/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Better Uptime&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;10 monitors&lt;/td&gt;
&lt;td&gt;~$24/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentry Crons&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;Yes (with Sentry)&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The alternatives in detail
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;u&gt;Best for: developers who need cron + heartbeat + uptime under one key&lt;/u&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tickstem&lt;/strong&gt;&lt;br&gt;
Tickstem bundles cron job scheduling, heartbeat monitoring, and uptime monitoring under a single API key. The heartbeat implementation is identical in concept to Healthchecks.io — each heartbeat gets a unique token, your job pings the URL after each run, and you get an alert after two consecutive missed intervals.&lt;/p&gt;

&lt;p&gt;The difference is what surrounds it. You can schedule the job from Tickstem directly, monitor the HTTP endpoint it hits, and receive all alerts in one place. There is a Go, Node.js, and Python SDK. The free tier includes 5 heartbeats, 5 uptime monitors, and 1,000 cron executions per month.&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;# Create a heartbeat&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.tickstem.dev/v1/heartbeats &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"nightly-export","interval_secs":86400,"grace_secs":3600}'&lt;/span&gt;

&lt;span class="c"&gt;# Ping it from your job&lt;/span&gt;
&lt;span class="nv"&gt;HEARTBEAT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-64-char-token"&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.tickstem.dev/v1/heartbeats/&lt;span class="nv"&gt;$HEARTBEAT_TOKEN&lt;/span&gt;/ping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: cron + heartbeat + uptime in one tool, API-first, Go/Node/Python SDKs, lower entry price than Cronitor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: newer product, smaller community than established alternatives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;u&gt;Best for: teams that want a polished all-in-one monitoring platform&lt;/u&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cronitor&lt;/strong&gt;&lt;br&gt;
Cronitor is the most direct feature-equivalent to the Tickstem bundle — it handles cron job monitoring, heartbeat monitoring, and uptime checks. It has been around longer and has a larger community. The dashboard is well-designed and the alerting is flexible.&lt;/p&gt;

&lt;p&gt;The tradeoff is price: Cronitor's paid plans start higher, and features like Slack alerting and team seats are gated behind more expensive tiers. If you are a team with a monitoring budget, Cronitor is a mature choice. If you are a solo developer or early-stage product, the cost is harder to justify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: mature product, strong community, comprehensive alerting options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: higher pricing, some features require paid tiers that are expensive for small teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;u&gt;Best for: simple heartbeat monitoring with minimal setup&lt;/u&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dead Man's Snitch&lt;/strong&gt;&lt;br&gt;
Dead Man's Snitch is the closest conceptual equivalent to Healthchecks.io. It does one thing: alert you when a job stops checking in. The free tier gives you one snitch — enough to evaluate the product. Setup is extremely simple.&lt;/p&gt;

&lt;p&gt;If you genuinely only need heartbeat monitoring and want the smallest possible tool, Dead Man's Snitch or Healthchecks.io are the right choices. Neither does cron scheduling or uptime monitoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: dead simple, focused, low cost entry point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: heartbeat-only, no uptime monitoring, no cron scheduling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;u&gt;Best for: teams that need uptime + heartbeat but not cron scheduling&lt;/u&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better Uptime&lt;/strong&gt;&lt;br&gt;
Better Uptime covers uptime monitoring and heartbeat checks in a clean interface with strong on-call and incident management features. If your primary concern is whether your API endpoints are responding and your background jobs are running — but you handle scheduling elsewhere — Better Uptime is a solid choice.&lt;/p&gt;

&lt;p&gt;It does not schedule cron jobs. Pricing is competitive for small teams but scales quickly as you add more monitors and team members.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: clean UI, good on-call features, uptime + heartbeat covered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: no cron scheduling, pricing escalates with team size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;u&gt;Best for: teams already using Sentry&lt;/u&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sentry Crons&lt;/strong&gt;&lt;br&gt;
If your application already sends errors to Sentry, Sentry Crons is the easiest addition. It monitors whether your cron jobs run on schedule and surfaces missed or failed jobs alongside your error tracking. It is included in Sentry's free tier.&lt;/p&gt;

&lt;p&gt;The limitation is scope — Sentry Crons does not monitor uptime for HTTP endpoints. It also ties you deeper into the Sentry ecosystem, which may or may not suit your stack. But if you are already paying for Sentry, adding cron and heartbeat monitoring costs nothing extra.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: free if you already use Sentry, good cron + heartbeat coverage, integrated with error tracking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: no uptime monitoring, requires Sentry, limited alerting channels on free tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosted Healthchecks
&lt;/h2&gt;

&lt;p&gt;Worth a separate mention: Healthchecks.io is fully open source and you can run it yourself. If you have a server already and want zero recurring cost for heartbeat monitoring, the self-hosted version is a legitimate option. The setup requires Docker or a Python environment, Postgres, and a mail server for alerts. For solo developers comfortable with server ops, this is hard to beat on cost.&lt;/p&gt;

&lt;p&gt;The downside is ongoing maintenance. Every upgrade, database migration, and backup falls on you. For a monitoring tool — something you depend on to tell you when things break — operational overhead is worth factoring in.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Just heartbeat monitoring, minimal cost&lt;/strong&gt;: Healthchecks.io free tier or self-hosted. Dead Man's Snitch if you want simpler setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heartbeat + uptime, no cron scheduling&lt;/strong&gt;: Better Uptime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heartbeat + cron scheduling + uptime, one tool&lt;/strong&gt;: Tickstem or Cronitor. Tickstem is cheaper for small teams; Cronitor is more mature for larger ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Already using Sentry&lt;/strong&gt;: Add Sentry Crons first — it costs nothing extra and covers most cases.&lt;/p&gt;

&lt;p&gt;The pattern that catches most developers is starting with Healthchecks.io for heartbeats, adding UptimeRobot for uptime, and eventually needing a cron scheduler — ending up with three separate tools. If you can see that combination in your future, starting with a bundle is worth the slightly higher entry cost.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>webdev</category>
      <category>programming</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Why email validation is harder than a regex</title>
      <dc:creator>Mike Tickstem</dc:creator>
      <pubDate>Wed, 20 May 2026 10:28:54 +0000</pubDate>
      <link>https://dev.to/mike_tickstem/why-email-validation-is-harder-than-a-regex-57ff</link>
      <guid>https://dev.to/mike_tickstem/why-email-validation-is-harder-than-a-regex-57ff</guid>
      <description>&lt;p&gt;The pattern &lt;code&gt;/.+@.+\..+/&lt;/code&gt; accepts &lt;code&gt;admin@mailinator.com&lt;/code&gt;, &lt;code&gt;test@defunct-domain.io&lt;/code&gt;, and &lt;code&gt;notreal@typo.vom&lt;/code&gt;. Here's what actually needs to happen before you trust an email address.&lt;/p&gt;

&lt;p&gt;Every sign-up form validates email addresses. Most do it with a regex. The regex catches obvious typos — missing &lt;code&gt;@&lt;/code&gt;, no domain — but it lets through a much larger class of addresses that will never receive a message. Those addresses quietly inflate your list, skew your open rates, and cost you money on every send.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four problems a regex can't catch
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The domain doesn't exist&lt;/strong&gt;. A user types &lt;code&gt;alice@gmial.com&lt;/code&gt;. The regex passes. The domain has no mail server. Your welcome email bounces. Your sender reputation drops. The domain &lt;code&gt;gmial.com&lt;/code&gt; used to be a common typo trap — attackers registered it to harvest credentials from misdirected password resets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The domain is real but the inbox is disposable&lt;/strong&gt;. Services like Mailinator, Guerrilla Mail, and a hundred others give anyone a working inbox with no sign-up. The email is valid — it will actually receive your message — but the address belongs to no one in particular. Anyone who knows the address can read it. Users create throwaway accounts to claim trial offers, avoid marketing, or test your app without commitment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The address is role-based&lt;/strong&gt;. &lt;code&gt;info@company.com&lt;/code&gt; is a real inbox shared by a team. &lt;code&gt;noreply@company.com&lt;/code&gt; likely discards everything. &lt;code&gt;admin@company.com&lt;/code&gt; probably goes to an ops rotation. These aren't personal addresses — they're organizational ones. Sending transactional or marketing email to role inboxes leads to low engagement and higher unsubscribe rates because nobody "owns" the address.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The domain exists but the mailbox doesn't&lt;/strong&gt;. SMTP probing — actually connecting to port 25 and issuing a &lt;code&gt;RCPT TO&lt;/code&gt; command — can answer this question. But most mail servers are configured to accept any address at the gateway and bounce later (a technique called catch-all), specifically to defeat probing. And most cloud providers block outbound port 25 entirely, so your probe never even reaches the server.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What you can actually do
&lt;/h2&gt;

&lt;p&gt;Given those constraints, a practical validation pipeline looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parse the syntax&lt;/strong&gt; — not just a regex, but a proper RFC 5322 parse. This catches malformed addresses and normalizes the format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look up MX records&lt;/strong&gt; — a DNS query confirms the domain has at least one mail exchanger. This is fast (&amp;lt;100ms), cheap, and reliable. It rejects typos like gmial.com and abandoned domains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check against a disposable domain list&lt;/strong&gt; — a maintained list of throwaway services. Mailinator, Guerrilla Mail, Trashmail, and several hundred others. This is a blocklist problem, not a detection one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flag role-based prefixes&lt;/strong&gt; — a simple match against known generic prefixes: admin, info, noreply, support, billing, etc. Flag rather than block — some legitimate users do sign up with a shared inbox.
Steps 1–3 give you a reliable answer. Step 4 gives you a signal. Together, they catch the vast majority of addresses that will cause you problems.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;The Tickstem verify SDK runs all four checks with one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/tickstem/verify"&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TICKSTEM_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// handle quota or network error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Valid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// result.Reason explains why: "invalid syntax", "no MX records found for domain",&lt;/span&gt;
    &lt;span class="c"&gt;// or "disposable email domain"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"email not accepted: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reason&lt;/span&gt;&lt;span class="p"&gt;)&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;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoleBased&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// your call — warn the user, or log and proceed&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is stored in your account history so you can audit it later. The API never probes the mail server — no port 25 calls, no contact with the recipient's infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to validate
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;At sign-up&lt;/strong&gt; — block disposable and invalid addresses before they enter your system. A user who can't sign up with a throwaway address either provides a real one or leaves. Both outcomes are better than a list full of ghost accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before a bulk send&lt;/strong&gt; — scrub your list against the API if it was collected through a form that didn't validate at the time. Sending to a high percentage of invalid addresses is the fastest way to get flagged by Gmail and Outlook's filtering algorithms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At re-engagement&lt;/strong&gt; — if an address hasn't opened anything in 90 days, re-verify before sending. MX records disappear. Domains get abandoned. What was valid a year ago may not be now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On SMTP probing&lt;/strong&gt;: Some services advertise mailbox-level verification via SMTP. In practice, catch-all servers make the result unreliable, port 25 is blocked on most cloud providers, and the latency (300ms–3s per check) makes it unsuitable for sign-up flows. MX + disposable + role-based covers most of what matters in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The verify API is available on all Tickstem plans. The Free tier includes 500 verifications per month. &lt;/p&gt;

&lt;p&gt;Go: &lt;code&gt;go get github.com/tickstem/verify&lt;/code&gt;. &lt;br&gt;
Node.js: &lt;code&gt;npm install @tickstem/verify&lt;/code&gt;. &lt;br&gt;
Python: &lt;code&gt;pip install tickstem&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to detect and block Mailinator emails (and 4,000+ disposable domains)</title>
      <dc:creator>Mike Tickstem</dc:creator>
      <pubDate>Wed, 20 May 2026 10:14:01 +0000</pubDate>
      <link>https://dev.to/mike_tickstem/how-to-detect-and-block-mailinator-emails-and-4000-disposable-domains-aea</link>
      <guid>https://dev.to/mike_tickstem/how-to-detect-and-block-mailinator-emails-and-4000-disposable-domains-aea</guid>
      <description>&lt;p&gt;Mailinator gives anyone a working inbox at any &lt;strong&gt;@mailinator.com&lt;/strong&gt; address with no signup, no password, no verification. For your application, that means free-tier abuse, fake signups, and a contact database full of addresses nobody owns. Here is what Mailinator actually is, why a blacklist alone is not enough, and how to block it reliably.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is Mailinator?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Mailinator is a free, public disposable email service.&lt;/strong&gt; Anyone can send mail to &lt;code&gt;anything@mailinator.com&lt;/code&gt; and read it at mailinator.com without registering, logging in, or proving ownership. Inboxes are public — if you know the address, you can read the mail. Messages auto-delete after a few hours.&lt;/p&gt;

&lt;p&gt;The service exists for a real reason. Developers use it to test signup flows, password resets, and transactional email without polluting personal inboxes. QA engineers use it to verify that delivery actually works. The problem is that the same property that makes it useful for testing — anyone can claim any address instantly — makes it the path of least resistance for anyone who wants to avoid giving you a real email.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mailinator email lookup: how it works in practice
&lt;/h3&gt;

&lt;p&gt;A user types &lt;code&gt;j8djk2@mailinator.com&lt;/code&gt; into your signup form. They open &lt;code&gt;https://www.mailinator.com/v4/public/inboxes.jsp?to=j8djk2&lt;/code&gt; in another tab. Your welcome email arrives there. They click the verification link, claim the free trial, and walk away. The address you stored in your database is shared with anyone on the internet who guesses or learns it.&lt;/p&gt;

&lt;p&gt;Mailinator also operates several alternative domains for users trying to get past simple blocklists: &lt;code&gt;@maildrop.cc&lt;/code&gt;, &lt;code&gt;@guerrillamail.com&lt;/code&gt;, &lt;code&gt;@throwawaymail.com&lt;/code&gt;, &lt;code&gt;@10minutemail.com&lt;/code&gt;, and dozens more. Mailinator itself sells a paid tier for legitimate enterprise testing with private inboxes — most of the public abuse comes from the free public side.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mailinator disposable email risks for your application
&lt;/h3&gt;

&lt;p&gt;The risks depend on what your application does, but they fall into three buckets:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Free trial and credit abuse&lt;/strong&gt;&lt;br&gt;
Every SaaS with a free trial faces this. A single person creates dozens of accounts using &lt;code&gt;name1@mailinator.com&lt;/code&gt;, &lt;code&gt;name2@mailinator.com&lt;/code&gt;, and so on. Each account claims the trial credits, free API calls, or starter quota. For a tool with a generous free tier and meaningful unit costs (AI inference, video processing, SMS), this drains real money. Stripe will not chargeback you because nobody charged anything — the damage is on the cost side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Compromised email-verification security&lt;/strong&gt;&lt;br&gt;
If your "email verification" step just confirms that someone can receive a message at the address, Mailinator passes it trivially. Anyone who can guess the address can read the verification link. Any account using a Mailinator address has no real owner — there is no second factor, no recovery channel that only one person controls. Treating these accounts as &lt;strong&gt;"verified"&lt;/strong&gt; gives a false sense of security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Marketing list and deliverability damage&lt;/strong&gt;&lt;br&gt;
Mailinator inboxes auto-delete. Messages are read once or not at all. If your sender reputation depends on engagement metrics (Gmail, Outlook, Yahoo all weight this heavily), having a meaningful chunk of your list at &lt;code&gt;@mailinator.com&lt;/code&gt; drags your domain reputation down. You can ship the best email campaign of your life and it will land in spam because your list has too many addresses nobody opens.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why a regex check is not enough
&lt;/h3&gt;

&lt;p&gt;The standard email regex like &lt;code&gt;/^[^@]+@[^@]+\.[^@]+$/&lt;/code&gt; happily accepts &lt;code&gt;fake@mailinator.com&lt;/code&gt;, &lt;code&gt;throwaway@guerrillamail.com&lt;/code&gt;, and &lt;code&gt;burner@10minutemail.com&lt;/code&gt;. The address is syntactically valid. The domain exists. Messages actually deliver. There is nothing the regex can catch — disposable email isn't a malformed address, it's a perfectly valid address you don't want.&lt;/p&gt;

&lt;p&gt;You need a different category of check entirely: looking up whether the domain itself belongs to a known disposable service.&lt;/p&gt;
&lt;h3&gt;
  
  
  Approach 1: Static blocklist
&lt;/h3&gt;

&lt;p&gt;The simplest defence is a list of disposable domains. Maintained lists exist on GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/disposable-email-domains/disposable-email-domains" rel="noopener noreferrer"&gt;disposable-email-domains/disposable-email-domains&lt;/a&gt; — about 4,000 domains, MIT licensed, updated monthly&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/disposable-email-domains/disposable-email-domains" rel="noopener noreferrer"&gt;martenson/disposable-email-domains&lt;/a&gt; — similar coverage, also community-maintained
Drop the list into your signup handler and reject any domain in it:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// JavaScript example — works the same in any language&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;disposableDomains&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./disposable-domains.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isDisposable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;"&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="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;disposableDomains&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)&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="nf"&gt;isDisposable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please use a non-disposable email address&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This blocks maybe &lt;strong&gt;80 percent&lt;/strong&gt; of the disposable traffic you will see. The reason it isn't 100 percent: disposable services launch new domains weekly. By the time your blocklist updates, the latest &lt;code&gt;@catchmail.io&lt;/code&gt; or &lt;code&gt;@spamgone.org&lt;/code&gt; has already been used to register hundreds of trial accounts. You are always slightly behind.&lt;/p&gt;
&lt;h3&gt;
  
  
  Approach 2: MX record lookup
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Mailinator and most disposable services share a small set of MX records.&lt;/strong&gt; A DNS query for the domain's mail exchangers reveals not just whether the domain receives mail (it does) but who runs that infrastructure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dig +short MX mailinator.com
&lt;span class="go"&gt;10 mail2.mailinator.com.
20 mail.mailinator.com.

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dig +short MX guerrillamail.com
&lt;span class="go"&gt;20 mx-guerrillamail.com.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combine MX-resolution with a pattern check on the MX hostname. A domain whose MX points at &lt;code&gt;mail.mailinator.com&lt;/code&gt; is clearly Mailinator regardless of what the apex domain is called. This catches white-label disposable services that share the same backend.&lt;/p&gt;

&lt;p&gt;This is also where you catch the second-biggest signup problem: &lt;strong&gt;domains with no MX records at all&lt;/strong&gt;. A typo like &lt;code&gt;user@gmial.com&lt;/code&gt; passes regex but fails MX lookup. Rejecting these saves you from bounces and the sender-reputation hit they cause.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 3: Full verification — MX, disposable, role-based, syntax
&lt;/h3&gt;

&lt;p&gt;The robust signup check combines all four signals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;RFC-5322 syntax parse&lt;/strong&gt; (not just a regex — catches subtle malformation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MX record lookup&lt;/strong&gt; (rejects typo domains and abandoned domains)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disposable domain match&lt;/strong&gt; (rejects Mailinator and ~4,000 others)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role-based prefix detection&lt;/strong&gt; (admin@, info@, noreply@ — flag but don't always block)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Tickstem verify API does all four with a single call. Here is the Go SDK example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/tickstem/verify"&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TICKSTEM_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"j8djk2@mailinator.com"&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"verify failed: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="c"&gt;// fall back to accept-and-warn, never block on outage&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;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Disposable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"please use a non-disposable email address"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MXFound&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"email domain does not appear to accept mail"&lt;/span&gt;&lt;span class="p"&gt;)&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;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoleBased&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"signup with role-based address: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// proceed but flag for review&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same check in 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;from&lt;/span&gt; &lt;span class="n"&gt;tickstem&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;VerifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VerifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;j8djk2@mailinator.com&lt;/span&gt;&lt;span class="sh"&gt;"&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;disposable&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;please use a non-disposable email address&lt;/span&gt;&lt;span class="sh"&gt;"&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mx_found&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email domain does not appear to accept mail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same in Node.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;VerifyClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@tickstem/verify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VerifyClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TICKSTEM_API_KEY&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;j8djk2@mailinator.com&lt;/span&gt;&lt;span class="dl"&gt;"&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disposable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;please use a non-disposable email address&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mxFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email domain does not appear to accept mail&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What about catch-all and unverifiable domains?
&lt;/h3&gt;

&lt;p&gt;A small caveat: &lt;strong&gt;mailbox-level verification (does this exact inbox exist?) is unreliable in 2026&lt;/strong&gt;. Most mail servers run in catch-all mode at the gateway, accepting any address for the domain and bouncing later. SMTP probing — actually connecting to port 25 and asking the server — gets blocked by every major cloud provider as part of anti-spam policy.&lt;/p&gt;

&lt;p&gt;For practical purposes, &lt;strong&gt;"the inbox accepts mail at the gateway"&lt;/strong&gt; is the strongest signal you can get without sending the message. MX + disposable + role-based covers the cases that actually matter for signup flows. Catch-all aside, you do not need port 25 probing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparison: blocklist vs MX vs full verification
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Approach&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Catches Mailinator&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Catches new disposable domains&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Catches typos&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Latency&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Regex only&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;0ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static blocklist&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;1ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MX lookup only&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;50–150ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full verification API&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;80–200ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;One important caveat:&lt;/strong&gt; never make signup completely dependent on a third-party API. If the verification service has an outage, your signup should fail open with a logged warning, not block legitimate users. Treat the result as a strong signal, not a hard gate when the network call itself fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to put in the rejection message
&lt;/h3&gt;

&lt;p&gt;The exact wording of the rejection message matters. "We do not accept Mailinator addresses" tells a knowledgeable attacker exactly which check you are running, and they will try the next service. Better:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generic and polite:&lt;/strong&gt; "Please use a non-disposable email address" — works, but obvious&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specific and informative:&lt;/strong&gt; "This appears to be a disposable inbox. Please use a work or personal email address" — most useful for honest users who typoed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for security-sensitive flows:&lt;/strong&gt; Accept the signup, mark the account as restricted, require additional verification (phone, SSO, credit card pre-auth) before unlocking the full feature set
The third option is the safest pattern for high-value services. You never tell the attacker exactly what tripped the check, and you do not lose legitimate users who happen to use a service you do not recognise.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Mailinator alternatives users will try
&lt;/h3&gt;

&lt;p&gt;For completeness, the most common disposable services users switch to when Mailinator is blocked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Guerrilla Mail&lt;/strong&gt; — &lt;code&gt;@guerrillamail.com&lt;/code&gt;, &lt;code&gt;@sharklasers.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10 Minute Mail&lt;/strong&gt; — &lt;code&gt;@10minutemail.com&lt;/code&gt;, several rotating domains&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temp Mail&lt;/strong&gt; — &lt;code&gt;@temp-mail.org&lt;/code&gt; and similar&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Yopmail&lt;/strong&gt; — &lt;code&gt;@yopmail.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maildrop&lt;/strong&gt; — &lt;code&gt;@maildrop.cc&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EmailOnDeck&lt;/strong&gt; — multiple rotating domains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these are covered by the maintained blocklists linked above. A blocklist-plus-MX-check stack catches them all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;The Tickstem verify API does the full check (syntax, MX, disposable, role-based) in a single call and returns a structured result. The Free tier includes 500 verifications per month — enough for most early-stage applications. Larger volumes are covered by Starter, Pro and Business plans.&lt;/p&gt;

&lt;p&gt;Install the SDK in your language of choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt;: &lt;code&gt;go get github.com/tickstem/verify&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt;: &lt;code&gt;npm install @tickstem/verify&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt;: &lt;code&gt;pip install tickstem&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also try the verification with a single curl call against our public endpoint — see &lt;a href="https://tickstem.dev/docs#verify-overview" rel="noopener noreferrer"&gt;the verification documentation&lt;/a&gt; for details.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>The status page that updates itself</title>
      <dc:creator>Mike Tickstem</dc:creator>
      <pubDate>Tue, 12 May 2026 15:18:46 +0000</pubDate>
      <link>https://dev.to/mike_tickstem/the-status-page-that-updates-itself-5gam</link>
      <guid>https://dev.to/mike_tickstem/the-status-page-that-updates-itself-5gam</guid>
      <description>&lt;p&gt;Most status pages lie. &lt;/p&gt;

&lt;p&gt;Not intentionally — they just require a human to update them, and humans are busy, stressed, and dealing with the incident when the incident happens. So the status page says "All systems operational" while users are hitting errors, and the truth comes out twenty minutes later when someone finally posts "We are investigating reports of..."&lt;/p&gt;

&lt;p&gt;The problem isn't laziness. It's that updating a status page is a manual step in the middle of an already chaotic situation. Your monitoring and your status page are disconnected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The obvious fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Connect them. If your status page is generated directly from your monitoring data, it updates the moment something goes wrong — no human required, no lag, no posts that trail the actual problem by 15 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tickstem&lt;/strong&gt; now ships status pages that do exactly this. Enable one in the dashboard, pick a slug, and your page is live at &lt;strong&gt;tickstem.dev/status/your-app&lt;/strong&gt;. It reflects the current state of your uptime monitors and heartbeat monitors automatically.&lt;/p&gt;

&lt;p&gt;No code required. No webhooks to wire up. If you already have monitors running, your status page is one toggle away.                                                                                                                                        &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it shows&lt;/strong&gt;                                                                                                                                                                                                                                               &lt;/p&gt;

&lt;p&gt;The page has two sections:                                                                                                                                                                                                                                  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Services&lt;/strong&gt; — your HTTP endpoints, pulled from uptime monitors, with current status and 30-day uptime percentage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scheduled Jobs&lt;/strong&gt; — your background jobs, pulled from heartbeat monitors, with whether they're currently running and when they last pinged in.&lt;/p&gt;

&lt;p&gt;That second section is where it gets interesting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;The gap other status pages miss&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A status page that only shows HTTP endpoints can tell you your server is responding. It can't tell you whether your nightly data sync ran, whether the daily report job completed, or whether the backup that was supposed to finish at 3am actually finished.&lt;/p&gt;

&lt;p&gt;Those are background jobs, and they fail silently. The server is healthy, the endpoint returns 200, but nothing useful is happening behind it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tickstem&lt;/strong&gt; heartbeat monitors solve this by inverting the check: your job pings a URL after every successful run, and if the pings stop arriving, you get alerted. The status page surfaces this signal publicly — so your users see not just "API: Operational" but "Weekly digest: Running" or "Nightly sync: Missing".&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;from&lt;/span&gt; &lt;span class="n"&gt;tickstem&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HeartbeatClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HeartbeatCreateParams&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HeartbeatClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;hb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HeartbeatCreateParams&lt;/span&gt;&lt;span class="p"&gt;(&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;nightly-sync&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;interval_secs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;grace_secs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# At the end of every successful run:
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that ping stops arriving, your status page flips to "&lt;strong&gt;Missing&lt;/strong&gt;" automatically.                                                                                                                                                                             &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Three steps to a live status page&lt;/em&gt;&lt;/strong&gt;                                                                                                                                                                                                                           &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Dashboard&lt;/strong&gt; → &lt;strong&gt;Status Page&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Choose a slug and set a title&lt;/li&gt;
&lt;li&gt;Toggle visibility to &lt;strong&gt;Public&lt;/strong&gt; (&lt;strong&gt;Private&lt;/strong&gt; mode could be used to preview or you can just keep the page private)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your monitors and heartbeats populate it automatically. Set it to Private first to preview before sharing — the URL works for you while logged in, invisible to everyone else.&lt;/p&gt;

&lt;p&gt;Free tier included. No credit card required.                                                                                                                                                                                                                &lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/tickstem" rel="noopener noreferrer"&gt;https://github.com/tickstem&lt;/a&gt;  &lt;/p&gt;

</description>
      <category>devops</category>
      <category>monitoring</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Your Python cron jobs are failing silently. Here's how to fix it.</title>
      <dc:creator>Mike Tickstem</dc:creator>
      <pubDate>Mon, 11 May 2026 20:07:18 +0000</pubDate>
      <link>https://dev.to/mike_tickstem/your-python-cron-jobs-are-failing-silently-heres-how-to-fix-it-n4j</link>
      <guid>https://dev.to/mike_tickstem/your-python-cron-jobs-are-failing-silently-heres-how-to-fix-it-n4j</guid>
      <description>&lt;p&gt;If you're running a Python app on Vercel, Railway, Render, or Fly.io, you've run into this - there's no persistent process, so you can't use cron, APScheduler, or Celery workers the traditional way. &lt;br&gt;
Most developers end up using the platform's built-in scheduled tasks — which work fine until they silently stop running and nobody notices for three days.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;Tickstem&lt;/strong&gt; to solve this, and just shipped a Python SDK.                                                                                                                                                                                              &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The silent failure problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the thing about cron jobs: they fail in two ways.                                                                                                                                                                                                    &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loud failures&lt;/strong&gt; — the job runs, your endpoint returns 500, you get an alert. Fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silent failures&lt;/strong&gt; — the job never runs at all. Maybe a deployment broke something, maybe a config changed. The endpoint is healthy, no errors anywhere, but the job just... stopped. You find out when a user asks why their weekly report didn't arrive.&lt;/p&gt;

&lt;p&gt;The second type is what kills you. Uptime monitoring doesn't catch it because your server is up. Error tracking doesn't catch it because there's no error. You need a dead man's switch.                                                                    &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install&lt;/strong&gt;                                                                                                                                                                                                                                                  &lt;/p&gt;

&lt;p&gt;&lt;code&gt;pip install tickstem&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Requires Python 3.11+. One package, one API key, four tools.                                                                                                                                                                                                &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scheduling cron jobs&lt;/strong&gt;                                                                                                                                                                                                                                       &lt;/p&gt;

&lt;p&gt;Instead of running a scheduler inside your app, Tickstem calls your HTTP endpoint on a schedule from outside:&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;from&lt;/span&gt; &lt;span class="n"&gt;tickstem&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CronClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CronRegisterParams&lt;/span&gt;                                                                                                                                                                                                         

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CronClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CronRegisterParams&lt;/span&gt;&lt;span class="p"&gt;(&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;send-weekly-report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0 9 * * 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# every Monday at 9am UTC
&lt;/span&gt;      &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://yourapp.com/jobs/weekly-report&lt;/span&gt;&lt;span class="sh"&gt;"&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;Your endpoint just needs to return 2xx. No SDK needed on the receiving side — it's just an HTTP call. This means it works on any serverless platform without touching your app's runtime.                                                                   &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dead man's switch (heartbeat monitoring)&lt;/strong&gt;                                                                                                                                                                                                              &lt;/p&gt;

&lt;p&gt;This is the part that actually solves the silent failure problem. Your job sends a ping after every successful run:&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;from&lt;/span&gt; &lt;span class="n"&gt;tickstem&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HeartbeatClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HeartbeatCreateParams&lt;/span&gt;                                                                                                                                                                                                 

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HeartbeatClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Create once, save the token                                                                                                                                                                                                                               
&lt;/span&gt;&lt;span class="n"&gt;hb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HeartbeatCreateParams&lt;/span&gt;&lt;span class="p"&gt;(&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;weekly-report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;interval_secs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;604800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# expect a ping every 7 days
&lt;/span&gt;      &lt;span class="n"&gt;grace_secs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;# 1 hour buffer before alerting
&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# At the end of your job handler:                                                                                                                                                                                                                           
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                                      
      &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# no API key needed — token is the credential
&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;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;heartbeat ping failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# non-fatal
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If pings stop arriving within the window, you get an email. That's it. The ping endpoint doesn't require your API key — just the token — so you can call it safely from any context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uptime monitoring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While you're at it, monitor your actual endpoints too:&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;from&lt;/span&gt; &lt;span class="n"&gt;tickstem&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UptimeClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UptimeCreateParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Assertion&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UptimeClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;monitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UptimeCreateParams&lt;/span&gt;&lt;span class="p"&gt;(&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;Production API&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.yourapp.com/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;interval_secs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;assertions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="nc"&gt;Assertion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comparison&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;200&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;                                                                                                                                                                                     
          &lt;span class="nc"&gt;Assertion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comparison&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&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;Assertions let you define what "healthy" actually means — not just that the server responded, but that it responded correctly and fast enough.                                                                                                              &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email verification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One more thing in the bundle — validate email addresses before storing them:&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;from&lt;/span&gt; &lt;span class="n"&gt;tickstem&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;VerifyClient&lt;/span&gt;                                                                                                                                                                                                                           

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VerifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;valid&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Email rejected: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;disposable&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Disposable email addresses are not allowed.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checks syntax, MX records, 200+ disposable domains, and role-based prefixes (admin@, noreply@, etc). No SMTP probing.                                                                                                                                       &lt;/p&gt;

&lt;p&gt;One API key for everything                                                                                                                                                                                                                                  &lt;/p&gt;

&lt;p&gt;All four tools share one API key and one plan. Free tier includes 1,000 cron executions, 5 uptime monitors, 5 heartbeats, and 500 email verifications per month.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pip install tickstem&lt;/code&gt;                                                                                                                                                                                                                                       &lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/tickstem/python" rel="noopener noreferrer"&gt;https://github.com/tickstem/python&lt;/a&gt;&lt;br&gt;
Docs: &lt;a href="https://tickstem.dev/docs" rel="noopener noreferrer"&gt;https://tickstem.dev/docs&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>serverless</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Heartbeat monitoring: know when your scheduled jobs silently stop working</title>
      <dc:creator>Mike Tickstem</dc:creator>
      <pubDate>Sun, 03 May 2026 02:53:12 +0000</pubDate>
      <link>https://dev.to/mike_tickstem/heartbeat-monitoring-know-when-your-scheduled-jobs-silently-stop-working-3b3e</link>
      <guid>https://dev.to/mike_tickstem/heartbeat-monitoring-know-when-your-scheduled-jobs-silently-stop-working-3b3e</guid>
      <description>&lt;p&gt;Uptime monitoring tells you when your server goes down. But some of the worst outages look like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The server is fine&lt;/li&gt;
&lt;li&gt;The cron scheduler fired&lt;/li&gt;
&lt;li&gt;Nothing visibly broke&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The job just quietly stopped doing anything useful.&lt;/p&gt;

&lt;p&gt;A nightly data sync that hasn't run in four days. A backup that "completed" but wrote zero bytes. A report job that started silently throwing exceptions three weeks ago. These failures are invisible to a traditional HTTP monitor because the endpoint&lt;br&gt;
never went down.&lt;/p&gt;

&lt;p&gt;This is what heartbeat monitoring solves.&lt;/p&gt;

&lt;p&gt;How it works&lt;/p&gt;

&lt;p&gt;A heartbeat monitor is a dead-man's switch. Instead of Tickstem polling your endpoint, your job calls Tickstem at the end of every successful run. If the ping stops arriving within the expected interval + grace window, you get an alert.&lt;/p&gt;

&lt;p&gt;You configure two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;interval — how often you expect a ping (e.g. every 24h)&lt;/li&gt;
&lt;li&gt;grace window — buffer past the deadline before alerting (e.g. 1h)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No ping for two consecutive intervals → alert sent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wiring it up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/tickstem/heartbeat"&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;heartbeat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TICKSTEM_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;hb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;heartbeat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateParams&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;         &lt;span class="s"&gt;"nightly-sync"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IntervalSecs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;GraceSecs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="m"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c"&gt;// at the end of every successful run — token is the credential, no API key needed&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"heartbeat ping failed:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// non-fatal&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Node.js:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HeartbeatClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@tickstem/heartbeat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HeartbeatClient&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TICKSTEM_API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heartbeat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nightly-sync&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;interval_secs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// at the end of every successful run&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;heartbeat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&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="o"&gt;=&amp;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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ping failed:&lt;/span&gt;&lt;span class="dl"&gt;"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Or just curl — no SDK needed:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.tickstem.dev/v1/heartbeats/&lt;span class="nv"&gt;$HEARTBEAT_TOKEN&lt;/span&gt;/ping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token goes in the URL. No auth header. If curl fails, the script still exits cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The thing worth noting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The ping only happens on success. Silence means something went wrong — either the job crashed, was never scheduled, or completed without doing its actual work. That's the point.&lt;/p&gt;

&lt;p&gt;Make the ping non-fatal though. A transient network blip shouldn't abort a successful sync.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Any job where "it ran" and "it did something useful" are different things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database backups&lt;/li&gt;
&lt;li&gt;Data sync / ETL pipelines&lt;/li&gt;
&lt;li&gt;Report generation&lt;/li&gt;
&lt;li&gt;Invoice or payment processing&lt;/li&gt;
&lt;li&gt;Cache warming&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Uptime monitoring and heartbeat monitoring are complementary. Uptime = server is alive. Heartbeat = job actually did its job.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>go</category>
      <category>node</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
