Contents

The Cloud Is Someone Else's Cron Job

Contents

I once watched a client’s finance team celebrate migrating a nightly report off “a fragile old cron job” onto “a modern serverless scheduled pipeline.” The new pipeline was a managed function triggered by a managed scheduler, writing to a managed queue, watched by a managed alerting service, at roughly forty times the monthly cost of the box it replaced. I asked what the scheduled function actually did on trigger. It ran a script at 2am and emailed a CSV. That’s cron. That has always been cron. The only thing that changed was who owned the machine running crond and how many layers of abstraction sat between the client and that fact.

This isn’t a complaint about managed services being bad — plenty of them buy you genuine value, mostly in the form of someone else staying awake at 3am so you don’t have to. It’s an observation about what’s actually happening underneath the product names, because understanding the mechanism changes how you evaluate whether you need the product at all. A remarkable amount of “the cloud” is a scheduler triggering a script on a timer, running on a machine you don’t operate, with someone else’s on-call rotation backing it up. Once you see that clearly, you can price the abstraction honestly instead of paying for it on faith.

What a scheduled cloud job actually is

Advertisement

Look under any managed “scheduled job,” “cron trigger” or “recurring workflow” product and you’ll find the same three parts every time: a timer that fires on a schedule, a piece of compute that runs when the timer fires, and a system that records whether the run succeeded. That’s the entire feature set. AWS EventBridge Scheduler, Google Cloud Scheduler, Azure Logic Apps triggers, GitHub Actions’ schedule: — all of them are crontab syntax or a close cousin of it, wrapped in a control plane, backed by someone else’s Linux boxes running the exact same cron daemon you’d run yourself:

1
2
3
# a managed scheduler's underlying trigger definition
# is functionally identical to a crontab line
0 2 * * * curl -X POST https://api.example.com/jobs/nightly-report

The value being sold isn’t the scheduling primitive — cron solved that in 1975 and hasn’t needed to change much since. The value is everything wrapped around it: retry logic when the run fails, a dashboard showing history without you building one, IAM policies controlling who can trigger it, and infrastructure that scales past a single box if the job needs to. Those are real, sometimes worthwhile conveniences. None of them are magic, and all of them can be replicated on hardware you own with tools that have existed for decades.

Building the same guarantees yourself

A self-hosted equivalent needs the same three parts: a scheduler, a runner, and a way to know when a run silently didn’t happen — the failure mode plain cron is famously bad at surfacing, since a script that dies at line three leaves no trace unless you built one. Systemd timers close part of that gap over raw crontab entries, because they log to the journal and integrate with systemctl status:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# /etc/systemd/system/nightly-report.timer
[Unit]
Description=Run the nightly report generator

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target
1
2
3
4
5
6
7
# /etc/systemd/system/nightly-report.service
[Unit]
Description=Generate and email the nightly report

[Service]
Type=oneshot
ExecStart=/usr/local/bin/generate-report.sh

Persistent=true covers the case a cloud scheduler quietly ignores: if the machine was off at 2am, the timer fires the moment it comes back, rather than silently skipping that day’s run entirely. That single line closes a gap that has genuinely caught out managed cron users too, on services that don’t backfill missed triggers.

The remaining gap is knowing whether the run actually succeeded, and that’s where self-hosted monitoring earns its keep. A self-hosted dead man’s switch for your cron jobs — or the hosted equivalent in Healthchecks.io watching over your cron jobs — pings an endpoint on success and alerts you the moment a scheduled ping doesn’t arrive on time. That’s the actual feature people are paying cloud providers for when they buy “managed scheduling with monitoring,” and it costs nothing more than a container and a cron line that already exists on your machine:

1
2
# add to the end of generate-report.sh
curl -fsS -m 10 --retry 3 https://healthchecks.example.local/ping/nightly-report

The bill you don’t see until you add it up

Advertisement

The actual cost comparison rarely gets made honestly because the two bills look completely different in shape. A managed scheduled function bills per invocation, per gigabyte-second of compute, and often per log line ingested by the accompanying monitoring product — three separate line items for what used to be one crontab entry. A self-hosted equivalent bills as a fraction of a machine you likely already own for other reasons: the marginal cost of one more systemd timer on a box already running your reverse proxy or your file server rounds to zero.

Where the cloud bill actually earns its keep is concurrency you can’t predict in advance. A report job that runs once a night at a fixed time is not that job. A webhook handler that might need to process one request or ten thousand in the same minute, depending on what a client does, genuinely benefits from infrastructure that scales elastically per invocation — and that’s a real, non-marketing reason serverless compute exists. The trouble is that most “scheduled job” migrations I’ve seen were the first kind wearing the justification of the second, because “scales infinitely” sounds better in a planning meeting than “runs once a night.”

What you actually give up by self-hosting the scheduler

None of this is a free lunch in the other direction either. Running your own timer means you own patching the box it runs on, you own the monitoring stack that tells you when it stops working, and you own recovering from the day the disk fills up at 1:58am and the 2am job never fires. A managed scheduler absorbs all three of those, silently, as part of what you’re paying for — and for an on-call rota you don’t have and can’t build, that absorption is worth real money.

The honest reckoning is to be specific about which of those three you’re actually short on. If patching Linux boxes is a solved problem in your organisation because you already run several, the cloud bill buys you almost nothing extra for a fixed nightly job. If nobody on the team has ever configured a systemd unit and never will, the ten-dollar managed function is the cheaper option once you count the labour honestly, and pretending otherwise for the sake of self-hosting purity is its own kind of trap.

Why the cloud version still wins for some jobs

None of this is an argument that self-hosting scheduled work is always the right call. The cloud version genuinely earns its cost in a few specific situations: when the job needs to burst to serious concurrency (fan-out to thousands of parallel invocations, which a single box handles poorly); when the job must survive the total loss of your own infrastructure, including your own network; or when your organisation already has nobody available to patch and monitor a Linux box and the ten-dollar managed function is unambiguously cheaper than the labour of running one. A team of two engineers with no ops capacity buying serverless scheduling for a genuinely bursty workload is a sound decision, not a marketing trap.

The trap is paying cloud prices for a workload with none of those properties: a single nightly script, running on a schedule, against infrastructure you already operate anyway. That’s the case where “the cloud” is quietly billing you monthly for a crontab line and someone else’s on-call engineer, and where Kubernetes CronJobs that don’t silently fail or a plain systemd timer on a box you already own delivers the identical guarantee for the cost of the electricity.

Event-driven triggers are the same trick with a different alarm clock

Not every managed “scheduled” service fires on a clock at all — plenty trigger on an event instead: a file landing in object storage, a message arriving on a queue, a webhook from a third party. It’s tempting to treat those as a fundamentally different category from cron, but the mechanism underneath is the same watcher pattern with a different alarm clock. Something polls or subscribes for a condition, and when the condition is true, it runs your code. inotify on Linux has done exactly this locally for decades:

1
2
3
4
5
# run a script the moment a file appears in a watched directory
inotifywait -m /srv/incoming -e create |
while read path action file; do
  /usr/local/bin/process-upload.sh "/srv/incoming/$file"
done

A managed “trigger on object creation” product is inotifywait with a distributed filesystem underneath it and a nicer web console on top. Recognising that doesn’t mean the managed version is pointless — object storage triggers genuinely matter once the storage itself is distributed across machines you don’t control, because inotify only watches a local filesystem. It does mean the mental model doesn’t change: a watcher, a condition, and code that runs when the condition is met, exactly like the timer-based version, just with the alarm clock replaced by an event bus.

Troubleshooting: the failure modes both versions share

  • The job “ran” but did nothing. True of managed and self-hosted schedulers alike — a scheduler firing successfully only proves the trigger worked, not that the payload did. Always monitor the job’s actual output or exit code, never just the trigger event. journalctl -u nightly-report.service shows you the real exit status; a managed dashboard showing a green “invocation succeeded” tick often hides the same blind spot.

  • The job ran twice. Overlapping runs happen when a slow run is still executing when the next trigger fires. Systemd handles this with Type=oneshot plus a lock, or explicitly with flock:

    1
    
    flock -n /tmp/nightly-report.lock /usr/local/bin/generate-report.sh
    

    Managed schedulers hit the identical problem and solve it with the identical primitive — a distributed lock — just hidden behind a checkbox in the console.

  • The job didn’t run and nobody noticed for a week. This is the one failure mode that neither cron nor most managed schedulers catch by default. A dead man’s switch is the fix in both worlds; the mechanism is a missed-ping alert, not a platform feature.

  • Clock drift causes the job to fire at the wrong time. Self-hosted boxes need NTP configured properly (timedatectl status should show NTP synchronized: yes); managed platforms handle this for you, which is one of the few places the abstraction earns its cost outright.

  • Clock drift makes both versions unreliable. A managed platform’s clock is somebody else’s problem to keep synchronised, but it can still drift relative to your expectations if you’re comparing timestamps across systems — always log the wall-clock time the job actually started, in both worlds, rather than trusting the scheduler’s own record of when it meant to fire.

A quick audit worth running this week

If any of this resonates, the exercise that actually pays off is going through your cloud billing console and listing every “scheduled function,” “cron trigger” or “scheduled workflow” line item next to what it does. For each one, write down: does it need to burst past what a single always-on box can handle? Does it need to survive total loss of infrastructure you already operate for other reasons? Is there genuinely nobody available to own a Linux box? If the honest answer to all three is no, that line item is a crontab entry wearing a much more expensive coat, and it’s worth the afternoon it takes to move it home.

Is it worth reaching for

The honest test comes down to whether the job in front of you needs elastic concurrency, disaster-grade resilience, or genuinely zero available ops time, rather than “cloud or self-hosted” as a blanket philosophy. Most nightly reports, backup triggers and housekeeping scripts need none of those. They need a timer, a runner and a way to know when the ping doesn’t arrive, and all three of those have existed on Linux for longer than most cloud providers have. Recognising a “serverless scheduled workflow” as cron with better branding doesn’t mean you should always self-host — it means you get to decide deliberately, instead of paying rent on an abstraction you never actually needed.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.