<?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: Cronjob.de</title>
    <description>The latest articles on DEV Community by Cronjob.de (cronjobde).</description>
    <link>https://dev.to/cronjobde</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%2Forganization%2Fprofile_image%2F15530%2Faf24a6b3-d1e7-4b4e-af9d-d865929e9642.png</url>
      <title>DEV Community: Cronjob.de</title>
      <link>https://dev.to/cronjobde</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cronjobde"/>
    <language>en</language>
    <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; &lt;a href="https://cron-job.org" rel="noopener noreferrer"&gt;cron-job.org&lt;/a&gt; (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>
