<?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: Joscha Schultze</title>
    <description>The latest articles on DEV Community by Joscha Schultze (@joschaschultze).</description>
    <link>https://dev.to/joschaschultze</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4202115%2F436e0b97-39fd-4b15-9ad3-cf20d652c8ec.jpg</url>
      <title>DEV Community: Joscha Schultze</title>
      <link>https://dev.to/joschaschultze</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joschaschultze"/>
    <language>en</language>
    <item>
      <title>How to let Claude, ChatGPT or Cursor manage your cron jobs</title>
      <dc:creator>Joscha Schultze</dc:creator>
      <pubDate>Sun, 15 Nov 2026 12:00:49 +0000</pubDate>
      <link>https://dev.to/cronjobde/how-to-let-claude-chatgpt-or-cursor-manage-your-cron-jobs-2133</link>
      <guid>https://dev.to/cronjobde/how-to-let-claude-chatgpt-or-cursor-manage-your-cron-jobs-2133</guid>
      <description>&lt;p&gt;Your AI assistant writes the backup script in a minute. Then you still have to schedule it somewhere, and you have to notice when it stops running.&lt;/p&gt;

&lt;p&gt;With MCP (Model Context Protocol), the assistant can do that part too. It creates the cron job, sets up monitoring, and later tells you which runs failed and why. The schedule runs on the cron service, so the chat doesn't have to stay open.&lt;/p&gt;

&lt;p&gt;I'll use Cronjob.de for the examples, because that's the service I run. Other hosted schedulers offer MCP servers as well, and the steps are similar.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MCP changes
&lt;/h2&gt;

&lt;p&gt;MCP lets an assistant call tools of an outside service. A cron service exposes tools such as &lt;code&gt;create_job&lt;/code&gt;, &lt;code&gt;create_heartbeat&lt;/code&gt; or &lt;code&gt;list_failures&lt;/code&gt;. You describe what you want, the assistant picks the tool and fills in the details. Most clients ask you before a call that changes something.&lt;/p&gt;

&lt;p&gt;The assistant doesn't need to know the exact syntax of the service, and you don't need to switch to a dashboard for small changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting the assistant
&lt;/h2&gt;

&lt;p&gt;The MCP address is &lt;code&gt;https://www.cronjob.de/bridge&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude (web and desktop):&lt;/strong&gt; add a custom connector under Settings → Connectors and paste the address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code:&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;claude mcp add --transport http cronjob https://www.cronjob.de/bridge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;/mcp&lt;/code&gt; inside Claude Code to sign in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ChatGPT:&lt;/strong&gt; add a custom connector with the same address, where your plan offers custom connectors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cursor and other MCP clients:&lt;/strong&gt; add it as a remote server, for example in &lt;code&gt;mcp.json&lt;/code&gt;:&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="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"cronjob"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"https://www.cronjob.de/bridge"&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;On first use a browser window opens. You sign in to your account and allow access (OAuth 2.1 with PKCE). No password or API key ends up in the assistant's configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can ask
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Call &lt;a href="https://example.com/cron.php" rel="noopener noreferrer"&gt;https://example.com/cron.php&lt;/a&gt; every 15 minutes."&lt;/strong&gt;&lt;br&gt;
The assistant creates the job with that schedule and shows you what it saved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"My backup runs at 3 am on my own server. Tell me if it doesn't."&lt;/strong&gt;&lt;br&gt;
This is a different case: the job runs elsewhere, so the service should watch for a signal instead of calling a URL. The assistant creates a heartbeat and gives you the line for your crontab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;0 3 * * *  /path/to/backup.sh&lt;span class="o"&gt;;&lt;/span&gt; curl -fsS -m 10 --retry 3 https://runcheck.de/&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;your-id&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/&lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;/$?&lt;/code&gt; at the end sends the exit code of the script. You get an alert when the backup fails, and also when it doesn't report at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Check whether my export job runs to the end."&lt;/strong&gt;&lt;br&gt;
A 200 from the trigger only means the request was accepted. With an end signal, the script reports when its last step is done, and the assistant gives you the line to add at the end of the script.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Which jobs failed yesterday, and why?"&lt;/strong&gt;&lt;br&gt;
The assistant reads the failures with their status codes and error messages and sums them up. For a quick morning check, that's often faster than opening the dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Monitor my website and show it on my status page."&lt;/strong&gt;&lt;br&gt;
Uptime checks and status pages work the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things worth checking
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Time zones.&lt;/strong&gt; Cronjob.de stores times in German time (Europe/Berlin). The assistant converts "9 am in New York" for you, but ask it to read the result back in your own time zone. Daylight saving time starts and ends on different dates in the US and in Europe, so a converted time can be off by an hour for a few weeks a year.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deleting.&lt;/strong&gt; The delete tool is marked as destructive and needs the exact name of the job, so a vague "clean up my jobs" can't wipe the account by accident. Still, read what the assistant wants to delete before you confirm. The history of a deleted job is gone too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limits and logs.&lt;/strong&gt; Access tokens last one hour, and refresh tokens rotate on every use. If a refresh token is used twice, the whole chain is revoked. There are at most 60 changing calls per hour, and every change is logged with time and tool. You can disconnect the assistant at any time in your account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test the alert once.&lt;/strong&gt; Break the URL on purpose, wait for the alert, then fix it. An alert you've never seen arrive is an assumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Without an AI subscription
&lt;/h2&gt;

&lt;p&gt;You don't need an assistant with MCP to skip the cron syntax. The account has its own assistant: paste the instructions from your host or plugin documentation, and it fills in address, method and schedule. That one is in the free plan as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who can use it
&lt;/h2&gt;

&lt;p&gt;The MCP connection is part of the Unlimited plan and of the 14-day trial every new account starts with. Setup details and the full list of tools are on &lt;a href="https://www.cronjob.de/en/connect-ai" rel="noopener noreferrer"&gt;Manage cron jobs with AI&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I run Cronjob.de, so keep that in mind when reading the setup part.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>cronjob</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why cron jobs fail: what 12 days of real scheduled calls show</title>
      <dc:creator>Joscha Schultze</dc:creator>
      <pubDate>Fri, 06 Nov 2026 15:42:37 +0000</pubDate>
      <link>https://dev.to/cronjobde/why-cron-jobs-fail-what-12-days-of-real-scheduled-calls-show-17o4</link>
      <guid>https://dev.to/cronjobde/why-cron-jobs-fail-what-12-days-of-real-scheduled-calls-show-17o4</guid>
      <description>&lt;p&gt;How often does a cron job actually fail? And when it does, why?&lt;/p&gt;

&lt;p&gt;I run a cron service that calls URLs on a schedule, so I can look at this from the outside for a lot of very different servers: shared hosting, WordPress sites, Laravel apps, small VPSs, serverless functions. For this post I analyzed every scheduled call from 4 to 15 October 2026, 12 days in total. Heartbeat pings and our own internal jobs are excluded, so this is purely "a scheduler calls your script over HTTP".&lt;/p&gt;

&lt;p&gt;The short version: &lt;strong&gt;most cron jobs are boring, a few are broken all the time, and the rare failures of healthy jobs mostly fix themselves.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Most cron jobs never fail
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;79.9 %&lt;/strong&gt; of cron jobs did not fail a single time in 12 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;12.0 %&lt;/strong&gt; failed on less than 1 % of their calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2.2 %&lt;/strong&gt; failed on 1 to 50 % of their calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5.9 %&lt;/strong&gt; failed on more than half of their calls, and &lt;strong&gt;4.4 %&lt;/strong&gt; failed on every single call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So eight in ten jobs are completely fine, and almost all the rest have the occasional hiccup. If you look only at jobs that basically work (under 50 % failures), the failure rate is &lt;strong&gt;0.3 %&lt;/strong&gt; of calls, about three in a thousand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failures are concentrated in very few jobs
&lt;/h2&gt;

&lt;p&gt;The 5 % of cron jobs with the most failures cause &lt;strong&gt;99.7 %&lt;/strong&gt; of all failures.&lt;/p&gt;

&lt;p&gt;That changes how you should read any "failure rate". The average across all jobs looks scary, but it's dominated by jobs that are simply dead. If you include them, the rate jumps from 0.3 % to over 9 % of all calls. Those jobs aren't flaky, they're broken, and they keep running because nobody looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dead jobs: mostly an address that no longer exists
&lt;/h2&gt;

&lt;p&gt;Of the jobs that failed on more than half of their calls, &lt;strong&gt;56 %&lt;/strong&gt; got a &lt;strong&gt;404&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The pattern is almost always the same: the site was rebuilt, moved to a new CMS, or a plugin was removed, and the cron job still points to the old file. Nothing crashes, the scheduler just gets "not found" forever.&lt;/p&gt;

&lt;p&gt;What helps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After a relaunch or migration, go through your cron jobs the same way you go through redirects.&lt;/li&gt;
&lt;li&gt;Make sure a 404 actually reaches someone. A job that fails silently for months is the real problem, not the 404 itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Healthy jobs: the script itself is the most common cause
&lt;/h2&gt;

&lt;p&gt;For jobs that normally work, these are the share of jobs that hit each error at least once in the 12 days:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Share of jobs&lt;/th&gt;
&lt;th&gt;What usually happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Error in the script (HTTP 500)&lt;/td&gt;
&lt;td&gt;5.4 %&lt;/td&gt;
&lt;td&gt;PHP error, memory limit, database briefly gone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection dropped&lt;/td&gt;
&lt;td&gt;3.5 %&lt;/td&gt;
&lt;td&gt;Server closed the connection midway, often under load or by a firewall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server overloaded (HTTP 503)&lt;/td&gt;
&lt;td&gt;2.7 %&lt;/td&gt;
&lt;td&gt;Web server too busy or in maintenance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty reply&lt;/td&gt;
&lt;td&gt;2.1 %&lt;/td&gt;
&lt;td&gt;Connection open, but no answer; usually the host killed the process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upstream without an answer (HTTP 502)&lt;/td&gt;
&lt;td&gt;1.8 %&lt;/td&gt;
&lt;td&gt;A proxy or CDN in front of the server got nothing usable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection refused&lt;/td&gt;
&lt;td&gt;1.6 %&lt;/td&gt;
&lt;td&gt;Nothing listening for a moment, e.g. during a restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS/HTTPS failed&lt;/td&gt;
&lt;td&gt;1.1 %&lt;/td&gt;
&lt;td&gt;Often a certificate being replaced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server unreachable&lt;/td&gt;
&lt;td&gt;0.6 %&lt;/td&gt;
&lt;td&gt;Network fault or server down&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things stand out. First, the number one cause is your own code, not the network. A 500 on a cron endpoint is usually the same bug you'd see in the browser, just at 3 a.m. Second, a big part of the list is "the server was briefly not there": overload, restarts, killed processes, proxies in between.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; let your cron endpoint print a short reason when it fails, e.g. &lt;code&gt;http_response_code(500); echo "Import failed: supplier API timed out";&lt;/code&gt;. Whatever logs or alerts your scheduler gives you, a one-line reason is worth more than a bare status code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two thirds of failures happen once and then fix themselves
&lt;/h2&gt;

&lt;p&gt;Of all failure phases, &lt;strong&gt;67 %&lt;/strong&gt; consisted of exactly one failed call. The next run worked again.&lt;/p&gt;

&lt;p&gt;That's an argument for two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't alert on the first failure for frequent jobs.&lt;/strong&gt; A job that runs every minute and fails once is usually noise. Alerting after two or three failures in a row cuts most false alarms without hiding real outages. For a job that runs once a night, alert on the first failure, because the next try is 24 hours away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry, but only when it's safe.&lt;/strong&gt; A refused connection, a DNS error or a failed TLS handshake means the request never reached your script, so a retry is harmless. A 502 or 503 usually means the same, but not always: a gateway can lose the response after your script has already committed its work. After a 500 or a timeout the script may have done half its work. Retrying an import or a payment run blindly can do it twice. For jobs like that, give each scheduled run its own ID, send the same ID with every retry, and let the script process each ID only once. If you can't do that, don't retry them automatically. &lt;em&gt;(Updated after a helpful comment from lastmileops below.)&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Time limits matter for the long tail
&lt;/h2&gt;

&lt;p&gt;Half of all cron jobs finished within &lt;strong&gt;4.4 seconds&lt;/strong&gt; on average, and &lt;strong&gt;13 %&lt;/strong&gt; took less than a second. But &lt;strong&gt;11 %&lt;/strong&gt; ran longer than 30 seconds on average. Those are mostly imports, exports and backups.&lt;/p&gt;

&lt;p&gt;For that group the time limit is the real risk. Shared hosts, PHP's &lt;code&gt;max_execution_time&lt;/code&gt;, reverse proxies and schedulers all have one, and when you hit it the script stops halfway. Typical fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Process in batches and remember where you stopped, so each call does a fixed amount of work.&lt;/li&gt;
&lt;li&gt;Or trigger the work over HTTP and let it run in the background (a queue, &lt;code&gt;fastcgi_finish_request()&lt;/code&gt;, a worker), then report the result with a heartbeat.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bot protection quietly blocks some jobs
&lt;/h2&gt;

&lt;p&gt;A small share of jobs (&lt;strong&gt;0.4 %&lt;/strong&gt;) got a Cloudflare challenge page instead of their script. For those, every call failed, because a scheduler can't solve a JavaScript or CAPTCHA check. If your site sits behind a WAF or CDN, allow your scheduler explicitly, ideally by its User-Agent or a verified-bot setting rather than by IP, since IPs can change.&lt;/p&gt;

&lt;p&gt;Free hosts are a special case: some answer every request without a browser with a 200 and a JavaScript cookie check. The scheduler sees "success", but the script never ran. If your job reports success and nothing happens, check what the response body actually contains.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Most cron failures are not mysterious.&lt;/strong&gt; A dead URL, a crashing script, a host that was briefly busy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dangerous jobs are the silent ones.&lt;/strong&gt; Almost all failures come from a few jobs that stay broken because nobody gets told.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert on patterns, not single blips&lt;/strong&gt;, and retry only what's safe to retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at the response, not just the status code.&lt;/strong&gt; Both for failures (why did it fail?) and for "successes" (did the script really run?).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full numbers, method and FAQ are on our data page: &lt;a href="https://www.cronjob.de/en/cron-job-failure-statistics" rel="noopener noreferrer"&gt;Why cron jobs fail: statistics&lt;/a&gt;. If you want to dig into a specific problem, I also wrote guides on &lt;a href="https://www.cronjob.de/en/cron-job-not-running" rel="noopener noreferrer"&gt;a cron job that isn't running&lt;/a&gt; and &lt;a href="https://www.cronjob.de/en/cron-job-retry" rel="noopener noreferrer"&gt;when to retry a failed cron job&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Method: all scheduled HTTP calls made by Cronjob.de between 4 and 15 October 2026, excluding heartbeat pings and our own jobs. "Failure" means an HTTP status of 400 or higher or a network error. Percentages only, rounded. I run Cronjob.de, so this is data from one service, not the whole internet.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cronjob</category>
      <category>devops</category>
      <category>webdev</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Monitoring cron jobs with heartbeats: how to notice the job that silently stopped</title>
      <dc:creator>Joscha Schultze</dc:creator>
      <pubDate>Fri, 30 Oct 2026 08:09:18 +0000</pubDate>
      <link>https://dev.to/cronjobde/monitoring-cron-jobs-with-heartbeats-how-to-notice-the-job-that-silently-stopped-a4b</link>
      <guid>https://dev.to/cronjobde/monitoring-cron-jobs-with-heartbeats-how-to-notice-the-job-that-silently-stopped-a4b</guid>
      <description>&lt;p&gt;The worst cron failure is not the one that throws an error. It's the one that simply doesn't happen.&lt;/p&gt;

&lt;p&gt;The backup that hasn't run since the server was migrated. The import that stopped when someone redeployed and the crontab wasn't part of the image. The cleanup script that hangs forever on a lock, so the next runs pile up behind it. Nothing crashes, no error mail goes out, and you find out weeks later when you actually need that backup.&lt;/p&gt;

&lt;p&gt;I run a cron service, and "it stopped and nobody noticed" is by far the most common story I hear. The fix is a simple pattern called a &lt;strong&gt;heartbeat&lt;/strong&gt; (also known as a dead man's switch).&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea in one sentence
&lt;/h2&gt;

&lt;p&gt;Your job sends a small "I'm done" signal to a URL every time it finishes, and a monitor alerts you when that signal &lt;strong&gt;doesn't&lt;/strong&gt; arrive on time.&lt;/p&gt;

&lt;p&gt;The important part is the direction. Classic monitoring asks "did something go wrong?". A heartbeat asks "did the thing that should have happened actually happen?". That catches every failure mode at once: the server is down, cron isn't running, the crontab is gone, the script crashed before it could report anything, the job hangs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The simplest version
&lt;/h2&gt;

&lt;p&gt;Say this is your nightly backup:&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;# crontab&lt;/span&gt;
30 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/local/bin/backup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add one request at the end, and only when the script succeeded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;30 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/local/bin/backup.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 10 &lt;span class="nt"&gt;--retry&lt;/span&gt; 3 https://example-monitor/your-check-id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; means the ping is only sent if &lt;code&gt;backup.sh&lt;/code&gt; exited with code 0. If the backup fails, no ping arrives and you get an alert.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-fsS&lt;/code&gt; keeps curl quiet but still shows real errors.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-m 10&lt;/code&gt; stops curl after 10 seconds, so a slow monitoring endpoint can never block your job.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--retry 3&lt;/code&gt; covers a short network hiccup, so you don't get a false alarm.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the monitor side you set two things: the &lt;strong&gt;schedule&lt;/strong&gt; (every day at 02:30) and a &lt;strong&gt;grace period&lt;/strong&gt; (for example 30 minutes). If no ping arrives by 03:00, you get notified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Report failures right away
&lt;/h2&gt;

&lt;p&gt;With only &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, a failed run is noticed when the grace period is over. If your monitor supports it, report the exit code directly instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;30 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/local/bin/backup.sh&lt;span class="p"&gt;;&lt;/span&gt; curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 10 &lt;span class="nt"&gt;--retry&lt;/span&gt; 3 https://example-monitor/your-check-id/&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$?&lt;/code&gt; is the exit code of &lt;code&gt;backup.sh&lt;/code&gt;. Many heartbeat tools accept it in the URL: &lt;code&gt;0&lt;/code&gt; counts as success, anything else as an immediate failure. One line, both cases covered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catch jobs that hang
&lt;/h2&gt;

&lt;p&gt;A job that never finishes is the sneakiest case: no success, no error, just a process sitting there. Send a signal when the job &lt;strong&gt;starts&lt;/strong&gt;, too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://example-monitor/your-check-id

curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 10 &lt;span class="nt"&gt;--retry&lt;/span&gt; 3 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;/start"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

/usr/local/bin/import.sh
&lt;span class="nv"&gt;STATUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;

curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 10 &lt;span class="nt"&gt;--retry&lt;/span&gt; 3 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$STATUS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="nv"&gt;$STATUS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the monitor knows the job began. If neither success nor failure follows within the grace period, it can tell you the run is stuck. As a bonus you get the duration of every run, which is great for spotting a job that slowly gets slower.&lt;/p&gt;

&lt;h2&gt;
  
  
  Send the output along
&lt;/h2&gt;

&lt;p&gt;When something fails, the first question is "what did it print?". If your monitor accepts a POST body, send the last lines of the log with the ping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/local/bin/import.sh &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/import.log 2&amp;gt;&amp;amp;1
&lt;span class="nv"&gt;STATUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 10000 /tmp/import.log | curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 10 &lt;span class="nt"&gt;--retry&lt;/span&gt; 3 &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @- &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$STATUS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the alert and the error message arrive together, and you don't have to SSH into the server to find out what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the grace period
&lt;/h2&gt;

&lt;p&gt;The grace period decides how fast you hear about problems versus how many false alarms you get.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Longer than the normal run time.&lt;/strong&gt; If the job usually takes 20 minutes and sometimes 40, a 15 minute grace period will page you for nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shorter than "too late".&lt;/strong&gt; If the export must be ready by 7:00 for the morning report, the alert has to come before that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For frequent jobs, allow a missed run.&lt;/strong&gt; A job every 5 minutes with a 10 minute grace period tolerates one slow or skipped run before alerting. That's usually what you want.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pitfalls I see again and again
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The ping breaks the job.&lt;/strong&gt; If the monitoring service is unreachable and your script runs with &lt;code&gt;set -e&lt;/code&gt;, a failing &lt;code&gt;curl&lt;/code&gt; can fail the whole job. Keep the timeout short and add &lt;code&gt;|| true&lt;/code&gt; when the ping is inside the script and must never stop it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pinging at the start instead of the end.&lt;/strong&gt; A single ping as the first line proves only that cron started the script. Put it at the end, or use the start/finish pair above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cron's minimal environment.&lt;/strong&gt; Cron runs with a very small &lt;code&gt;PATH&lt;/code&gt;. A job that works in your terminal can fail under cron because &lt;code&gt;curl&lt;/code&gt; or your interpreter isn't found. Use absolute paths, e.g. &lt;code&gt;/usr/bin/curl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time zones.&lt;/strong&gt; The server's cron may run in UTC while you configured the monitor in local time, or the other way around. Then every alert comes an hour or two "late" and everyone learns to ignore them. Check both sides once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alerts that go nowhere.&lt;/strong&gt; An alert to &lt;code&gt;root@localhost&lt;/code&gt; or to the inbox of a colleague who left last year is the same as no alert. Send it to a channel people actually read, like a team mailbox, Slack or Teams, and test it once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overlapping runs.&lt;/strong&gt; If a run takes longer than the interval, two copies can run at once. Wrap the job in &lt;code&gt;flock -n /tmp/import.lock&lt;/code&gt; so a second copy exits immediately instead of piling up, and let the heartbeat tell you when runs get too long.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools
&lt;/h2&gt;

&lt;p&gt;The pattern is simple enough to build yourself, but then you have to monitor your monitor. Some ready-made options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://healthchecks.io" rel="noopener noreferrer"&gt;Healthchecks.io&lt;/a&gt;: open source, you can use the hosted version or run it yourself.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cronitor.io" rel="noopener noreferrer"&gt;Cronitor&lt;/a&gt;: commercial cron and job monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.cronjob.de/cron-job-monitoring" rel="noopener noreferrer"&gt;Cronjob.de&lt;/a&gt;: that's mine. Heartbeats with &lt;code&gt;/start&lt;/code&gt;, &lt;code&gt;/fail&lt;/code&gt; and the exit code in the URL as shown above, POST output up to 10 KB, alerts by e-mail, and on the Unlimited plan also via Slack, Teams, Discord, Telegram or webhook. The free plan includes one heartbeat with e-mail alerts, servers are in the EU.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whichever you choose: set up one heartbeat for the job you'd be most embarrassed to find broken. For most of us that's the backup.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I run Cronjob.de, a cron service from Germany, so I'm obviously not neutral about the tools section. The pattern itself works with any of them.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cronjob</category>
      <category>devops</category>
      <category>monitoring</category>
      <category>linux</category>
    </item>
    <item>
      <title>How to run cron jobs without a server: 4 ways and when to use which</title>
      <dc:creator>Joscha Schultze</dc:creator>
      <pubDate>Fri, 23 Oct 2026 09:03:07 +0000</pubDate>
      <link>https://dev.to/cronjobde/how-to-run-cron-jobs-without-a-server-4-ways-and-when-to-use-which-50da</link>
      <guid>https://dev.to/cronjobde/how-to-run-cron-jobs-without-a-server-4-ways-and-when-to-use-which-50da</guid>
      <description>&lt;p&gt;Sooner or later almost every web project needs something to happen on a schedule: send the newsletter at 8, clean up old sessions every night, import a price feed every 15 minutes, run &lt;code&gt;wp-cron.php&lt;/code&gt; reliably.&lt;/p&gt;

&lt;p&gt;On a VPS you open &lt;code&gt;crontab -e&lt;/code&gt; and you're done. But many projects don't have that: shared hosting without SSH, a serverless app, a static site with an API route, a WordPress or Shopware shop where the host doesn't offer cron.&lt;/p&gt;

&lt;p&gt;I've been running a cron service for many years, so I see these setups every day. The most common question I get is still: “It works when I open the URL in my browser, so why does it fail on schedule?” Usually it’s a timeout, a login wall or a firewall blocking unknown IPs. Here are the four approaches that actually work, and where each one stops working.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one question that decides it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can your job be triggered by an HTTP request?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Yes.&lt;/strong&gt; It's a PHP script, an API route, a webhook, &lt;code&gt;wp-cron.php&lt;/code&gt;. Then you only need something that &lt;em&gt;calls a URL&lt;/em&gt; on a schedule. That's options 1 to 3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No.&lt;/strong&gt; It's a Python script, a binary, a data pipeline that needs its own runtime. Then you need a platform that &lt;em&gt;runs your code&lt;/em&gt;. That's option 4.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most "I need a cron job" questions on shared hosting are the first kind, even if they don't look like it at first.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. An online cron service (URL calls on a schedule)
&lt;/h2&gt;

&lt;p&gt;You enter a URL and a schedule, the service calls it, logs the response and tells you when it fails.&lt;/p&gt;

&lt;p&gt;Typical setup for a PHP job on shared hosting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// cron/cleanup.php: protect it with a secret, it's a public URL&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'key'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nb"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CRON_KEY'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// ... do the work ...&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deleted 42 sessions"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then schedule &lt;code&gt;https://example.com/cron/cleanup.php?key=…&lt;/code&gt; every hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Services:&lt;/strong&gt; cron-job.org (free, run from Germany), &lt;a href="https://www.easycron.com" rel="noopener noreferrer"&gt;EasyCron&lt;/a&gt;, &lt;a href="https://www.cronjob.de" rel="noopener noreferrer"&gt;Cronjob.de&lt;/a&gt; (that's mine: free tier and paid plans, servers in the EU).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; shared hosting, WordPress (&lt;code&gt;wp-cron.php&lt;/code&gt;), Laravel's &lt;code&gt;schedule:run&lt;/code&gt; behind a route, any API endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timeouts.&lt;/strong&gt; The service waits a limited time for your response (often 30 to 60 seconds). Long jobs should return quickly and keep working in the background, or be split into batches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security.&lt;/strong&gt; The URL is public. Use a secret key or basic auth, and don't put real credentials in the query string if you can avoid it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"200 OK" isn't "it worked".&lt;/strong&gt; If your script catches an error and still prints a page, the call looks successful. Return a non-2xx status code on failure, or check the response body for an expected text. Some services can do that for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. GitHub Actions on a schedule
&lt;/h2&gt;

&lt;p&gt;If your code is on GitHub anyway, a workflow can run on a cron schedule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/nightly.yml&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;17&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;   &lt;span class="c1"&gt;# 03:17 every day&lt;/span&gt;
      &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Europe/Berlin"&lt;/span&gt;   &lt;span class="c1"&gt;# default is UTC&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;       &lt;span class="c1"&gt;# allows manual and external triggers&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/nightly.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; jobs that run your repository's code, a few times per day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's not precise.&lt;/strong&gt; GitHub says scheduled runs "can be delayed during periods of high loads", and under enough load queued jobs may be dropped. The shortest interval is 5 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled workflows are disabled&lt;/strong&gt; in public repositories after 60 days without repository activity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The default time zone is UTC.&lt;/strong&gt; Add a &lt;code&gt;timezone&lt;/code&gt; to the schedule (as above) if the job should follow local time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If timing matters, a common trick is to keep the &lt;code&gt;workflow_dispatch&lt;/code&gt; trigger and start the workflow from outside through the GitHub API on a reliable schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Your cloud provider's scheduler
&lt;/h2&gt;

&lt;p&gt;AWS EventBridge Scheduler, Google Cloud Scheduler and Azure Logic Apps all call an HTTP endpoint or a function on a cron schedule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; projects already running on that cloud, with IAM, retries and logging in one place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt; setup effort (accounts, roles, billing) that is out of proportion if all you need is "call this URL every 10 minutes". Pricing is usually small but not always zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A platform that runs your code
&lt;/h2&gt;

&lt;p&gt;Render, Railway, Fly.io, Kuberns and similar platforms have "cron job" services: you point them at a repository, give them a command and a schedule, and they start a container for each run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; jobs that are &lt;em&gt;not&lt;/em&gt; an HTTP endpoint: Python scripts, workers, anything that needs its own runtime and more time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt; cold starts, per-run costs, and that the job runs in a separate environment from your app, so it needs its own access to your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part everyone forgets: knowing when a job &lt;em&gt;didn't&lt;/em&gt; run
&lt;/h2&gt;

&lt;p&gt;All four options answer "how do I start the job?". None of them, by default, answers "how do I notice when it silently stopped?"&lt;/p&gt;

&lt;p&gt;The pattern for that is a &lt;strong&gt;heartbeat&lt;/strong&gt; (also called a dead man's switch): at the end of each successful run, the job pings a URL. If the ping doesn't arrive in time, you get an alert.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# crontab on your own server&lt;/span&gt;
0 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; php import.php &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; https://runcheck.de/abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; matters: the ping only goes out if the job exited successfully. This works for jobs on your own server, in GitHub Actions, n8n or anywhere else that can make an HTTP request.&lt;/p&gt;

&lt;p&gt;Dedicated tools for this are &lt;a href="https://healthchecks.io" rel="noopener noreferrer"&gt;Healthchecks.io&lt;/a&gt;, Cronitor and Dead Man's Snitch. Some cron services (including mine) combine both: they call your URLs &lt;em&gt;and&lt;/em&gt; watch the heartbeats of jobs that run elsewhere, so everything scheduled is in one list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shared hosting, PHP, WordPress, an API route&lt;/td&gt;
&lt;td&gt;Online cron service (1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code lives on GitHub, timing can be loose&lt;/td&gt;
&lt;td&gt;GitHub Actions (2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Already deep in AWS/GCP/Azure&lt;/td&gt;
&lt;td&gt;Cloud scheduler (3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not an HTTP endpoint, needs its own runtime&lt;/td&gt;
&lt;td&gt;Code-running platform (4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jobs already run somewhere, but you don't trust them&lt;/td&gt;
&lt;td&gt;Add heartbeat monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I run &lt;a href="https://www.cronjob.de" rel="noopener noreferrer"&gt;Cronjob.de&lt;/a&gt;, one of the services mentioned in option 1. I've tried to describe all options as they are. If I got something wrong about another tool, tell me in the comments and I'll fix it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cronjob</category>
      <category>webdev</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
