<?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 Gorman</title>
    <description>The latest articles on DEV Community by Mike Gorman (@smplkit).</description>
    <link>https://dev.to/smplkit</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%2F3967982%2Fc4df7b2b-a3bc-46f7-be5e-174ecb7a7db5.png</url>
      <title>DEV Community: Mike Gorman</title>
      <link>https://dev.to/smplkit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/smplkit"/>
    <language>en</language>
    <item>
      <title>Which job schedulers fire on time? We tested ten.</title>
      <dc:creator>Mike Gorman</dc:creator>
      <pubDate>Sat, 01 Aug 2026 11:37:23 +0000</pubDate>
      <link>https://dev.to/smplkit/which-job-schedulers-fire-on-time-we-tested-ten-e42</link>
      <guid>https://dev.to/smplkit/which-job-schedulers-fire-on-time-we-tested-ten-e42</guid>
      <description>&lt;p&gt;One of the application infrastructure products we built for &lt;a href="https://www.smplkit.com" rel="noopener noreferrer"&gt;smplkit&lt;/a&gt; is an HTTP job scheduler called &lt;a href="https://www.smplkit.com/products/jobs" rel="noopener noreferrer"&gt;Smpl Jobs&lt;/a&gt;. Of course, we wanted it to compare favorably with competing job schedulers, but we wondered how well it really stacked up in terms of what matters: making HTTP requests &lt;strong&gt;on time&lt;/strong&gt;. We decided to find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contestants
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/eventbridge/scheduler/" rel="noopener noreferrer"&gt;AWS EventBridge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cloudflare.com/products/workers/" rel="noopener noreferrer"&gt;Cloudflare Workers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cron-job.org" rel="noopener noreferrer"&gt;cron-job.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.easycron.com" rel="noopener noreferrer"&gt;EasyCron&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/scheduler" rel="noopener noreferrer"&gt;Google Cloud Scheduler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://posthook.io" rel="noopener noreferrer"&gt;Posthook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://upstash.com/qstash" rel="noopener noreferrer"&gt;QStash&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://runhooks.app/" rel="noopener noreferrer"&gt;Runhooks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.smplkit.com/products/jobs" rel="noopener noreferrer"&gt;Smpl Jobs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The idea was simple: fire a request at the top of the hour at some public endpoint and capture the date/time it arrives. The number of milliseconds past the hour is the skew: the lower the skew, the more “on time” the scheduler is.&lt;/p&gt;

&lt;p&gt;While the idea may be simple, actually finding a public endpoint schedulers can hit that will also capture and store the received time was not so simple. Ultimately, what we were looking for was some general-purpose benchmark-hosting website that we could &lt;code&gt;POST&lt;/code&gt; JSON messages to that would identify the benchmark, the subject, and record the date/time the request arrived. We couldn’t find anything that worked the way we wanted it to so we built one. Three weeks later, &lt;a href="https://www.smplmark.org" rel="noopener noreferrer"&gt;smplmark.org&lt;/a&gt; was open for business and ready to start hitting with the schedulers we wanted to test.&lt;/p&gt;

&lt;p&gt;We created the benchmark, defined the subjects, and then configured each scheduler to fire a request at smplmark’s &lt;a href="https://app.smplmark.org/api-reference#tag/measurements/POST/api/v1/measurements" rel="noopener noreferrer"&gt;measurement API&lt;/a&gt; with a JSON payload identifying the benchmark, the run, and the ID of the scheduler being tested. smplmark records the date/time each measurement was created; a separate &lt;code&gt;skew&lt;/code&gt; metric is derived from that as &lt;code&gt;created_at mod 3600000&lt;/code&gt; which gives us the number of milliseconds past the top of the hour.&lt;/p&gt;

&lt;p&gt;Every contestant runs on its free tier — or close to it: a couple of them bill us about a dollar a month. Partly to keep the field level; mostly to keep the bill near zero.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.smplkit.com/blog/why-i-built-smplmark" rel="noopener noreferrer"&gt;Why I built smplmark.org&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A few flaws
&lt;/h2&gt;

&lt;p&gt;The methodology has four flaws we know about:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Skew is computed modulo the hour, so a scheduler that fires exactly &lt;code&gt;60.1&lt;/code&gt; minutes late will actually look like it’s only &lt;code&gt;6 seconds&lt;/code&gt; late. It’s a corner case: the only contestant late enough to threaten the wrap is GitHub Actions, and they miss by wild margins, not by seconds. The wrap cuts the other way too: a request that arrives early would read as nearly an hour late. But the only arrivals anywhere near the wrap belong to GitHub Actions, and their unreliability is obvious (and &lt;a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule" rel="noopener noreferrer"&gt;well-documented&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;The endpoint is in US-East so schedulers running in US-West, or Europe, will pay a network tax the US-East schedulers don’t pay. But it’s only a few milliseconds; the scheduler’s engine is ultimately what dominates.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.smplmark.org" rel="noopener noreferrer"&gt;smplmark.org&lt;/a&gt; itself runs on Cloudflare so Cloudflare Workers may have a home-field advantage; its request may never leave Cloudflare’s network.&lt;/li&gt;
&lt;li&gt;The skew trusts smplmark’s clock. smplmark runs on Cloudflare, whose clocks are NTP-synced; we don’t independently verify them. If that clock drifted, every subject would shift by the same amount — the ordering would survive, the absolute numbers wouldn’t. A scheduler’s own clock is not corrected for: firing when your clock says it’s time is part of what’s being measured.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The leaderboard
&lt;/h2&gt;

&lt;p&gt;Here’s where things currently stand. The images below are pinned snapshots, so the numbers we cite stay next to the charts they came from; the live board updates every hour and will have moved by the time you read this. We removed GitHub Actions from this chart because, at its scale, the other bars stop being visible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.smplmark.org/benchmarks/smplkit.com/scheduler-latency" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feqb8xil1ebcekiuiucv3.png" alt="Live bar chart of median scheduler skew in milliseconds, one bar per scheduler, sorted lowest to highest, GitHub Actions excluded for scale" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Median skew in milliseconds past the top of the hour as of July 31. &lt;a href="https://www.smplmark.org/benchmarks/smplkit.com/scheduler-latency" rel="noopener noreferrer"&gt;Open the live board&lt;/a&gt; for current numbers and methodology.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers say
&lt;/h2&gt;

&lt;p&gt;Posthook lands closest to the mark with a median skew of &lt;code&gt;656 ms&lt;/code&gt; followed closely by Runhooks at &lt;code&gt;749 ms&lt;/code&gt;. QStash and Smpl Jobs are locked in a dead heat at around a second and a half and, behind them, EasyCron, Google Cloud Scheduler, and cron-job.org all consistently fire within about &lt;code&gt;4 to 8 seconds&lt;/code&gt; of the requested time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.smplmark.org/benchmarks/smplkit.com/scheduler-latency" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0l0suzmbt9xnr0fz3wsw.png" alt="Line chart of scheduler skew, one line per scheduler, GitHub Actions excluded for scale: the tightest lines hug zero, several sit seconds above the mark, one dead-flat line sits at about 26,500 milliseconds, and another holds about 33,000 before jumping to about 51,000 in the final hours" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four days of arrivals ending the morning of July 31, one point per arrival, GitHub Actions excluded for scale.&lt;/p&gt;

&lt;p&gt;Curiously, AWS EventBridge arrives almost exactly &lt;code&gt;26.5 seconds&lt;/code&gt; late. Every arrival we’ve logged sits inside a band a few hundred milliseconds wide; you could set a watch by exactly how late it is. In fairness, EventBridge exists to fan events out at enormous scale, and it’s good at that.&lt;/p&gt;

&lt;p&gt;Cloudflare Workers is also consistently late: typically about &lt;code&gt;33 seconds&lt;/code&gt; past the hour — though a handful of runs this week drifted toward &lt;code&gt;51 seconds&lt;/code&gt;. The one contestant whose request we thought might never leave its own network still shows up 33 seconds late, so no home-field advantage for them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.smplmark.org/benchmarks/smplkit.com/scheduler-latency" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feerfl1aq031tzh7hzfdn.png" alt="Table of scheduler skew statistics in milliseconds: average, median, min, max, p95, p99, and run count for every scheduler on the board, sorted by median" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Min, max, and percentiles for everyone on the board, GitHub Actions included, as of July 31. The units are still milliseconds.&lt;/p&gt;

&lt;p&gt;GitHub Actions median skew is about &lt;code&gt;27 minutes&lt;/code&gt; and that is being generous. More than half the time, GitHub Actions didn’t even fire. In fairness, GitHub tells you this: scheduled workflows are &lt;a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule" rel="noopener noreferrer"&gt;documented as best-effort&lt;/a&gt; and get delayed or dropped when load is high. They are not wrong. And you don’t have to take our word for it: [the workflow we use is public (&lt;a href="https://github.com/smplkit/benchmarks/actions/workflows/cron-skew.yml" rel="noopener noreferrer"&gt;https://github.com/smplkit/benchmarks/actions/workflows/cron-skew.yml&lt;/a&gt;), and GitHub’s own execution record is right there on its Actions tab — including the hourly gaps where runs simply never happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  We’re not number one — and we’re okay with that
&lt;/h2&gt;

&lt;p&gt;When we saw our own scheduler’s numbers coming in, consistently about 1.5 seconds late, we analyzed the code for where we might make some optimizations. We toyed with having it just start a bit early; if we’re consistently 1.5 seconds late, why not just start 1.4 seconds early? But we’re not &lt;em&gt;always&lt;/em&gt; 1.5 seconds late. The “start early” strategy caused us to sometimes fire a few milliseconds early, which we felt was probably &lt;em&gt;worse&lt;/em&gt; than being 1.5 seconds late. So we pulled out the early-start and decided to let the chips fall where they may. We’ll keep working on improving our numbers but, in the meantime, only Posthook’s worst run beats &lt;a href="https://www.smplmark.org/benchmarks/smplkit.com/scheduler-latency?view=table&amp;amp;sort=max" rel="noopener noreferrer"&gt;our worst run&lt;/a&gt; so that’s not too bad.&lt;/p&gt;

&lt;h2&gt;
  
  
  But don’t take our word for it
&lt;/h2&gt;

&lt;p&gt;I know, I know. A benchmark created by smplkit, hosting results on a website created by smplkit, that just so happens to show Smpl Jobs among the top “on time” job schedulers available. We know how it looks. But that is exactly why we created &lt;a href="https://www.smplmark.org" rel="noopener noreferrer"&gt;smplmark.org&lt;/a&gt;. Anyone in the world can create an account&lt;br&gt;
and do exactly what we did. Send an email to &lt;a href="mailto:support@smplkit.com"&gt;support@smplkit.com&lt;/a&gt; if you need help.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you care?
&lt;/h2&gt;

&lt;p&gt;Any job scheduler that fires within a few seconds of the scheduled time is probably just fine for most teams. 9 of the 10 schedulers we tested will fire within &lt;code&gt;60 seconds&lt;/code&gt; of the scheduled time and 7 of 10 land within &lt;code&gt;10 seconds&lt;/code&gt;. But for the most on-time execution, Posthook, Runhooks, Smpl Jobs, and QStash all typically land within a couple of seconds of the requested time.&lt;/p&gt;

&lt;p&gt;But firing “on the second” is just one consideration. If you care about response capture, run history, timezones, retry policies, and SDKs, check out &lt;a href="https://www.smplkit.com/blog/best-cron-job-services-2026" rel="noopener noreferrer"&gt;The best cron job services for 2026&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>showdev</category>
      <category>programming</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
