How Infra Teams Actually Schedule Work in 2026

· 8 min read

Cron is over fifty years old. Nothing has formally deprecated it, several tools have replaced it in some contexts, and it’s still the first thing many engineers reach for when something needs to run at 3am. That says something about the scheduling problem: each replacement solved part of it and left the rest.

There’s no dataset behind this post. There’s no reliable source for a figure like “X% of teams still use cron,” so this post doesn’t give one. What follows comes from how each system is built and what’s publicly documented about it.

Why cron persists

Cron’s entire interface is one file per user (or one system-wide file with a user column) and five fields of syntax that hasn’t meaningfully changed since Vixie cron in the late 1980s. It has no daemon-specific client to install, no API to learn, no YAML to write. It ships by default on effectively every Linux distribution and every macOS install. If a machine exists, cron can probably already schedule something on it, right now, with a command you already half-remember.

Many scheduling needs are simple: run this script nightly, rotate this log weekly, ping this endpoint every five minutes. Cron is enough for that, and anything heavier is often over-engineering. Tools that tried to replace cron outright, rather than in one context, mostly failed to displace it because they added ceremony cron doesn’t have, in exchange for capabilities most single-machine scheduling doesn’t need.

Cron’s real shortfalls are outside the scheduler itself: it won’t tell you when a job failed, it can’t coordinate a job across machines, and it gives you no single place to see what’s scheduled across a fleet short of SSHing into each box and running crontab -l. Those gaps are in the operational layer around cron, and have nothing to do with its syntax.

Where systemd timers won

systemd timers are a legitimate improvement over cron for the specific things systemd is already responsible for on a modern Linux box: service supervision, structured logging, dependency ordering. A .timer unit paired with a .service unit gets you output automatically captured in the journal (journalctl -u myjob.service) without the redirection boilerplate cron requires, dependency expressions (Requires=, After=) that let a job wait on another unit, and Persistent=true semantics that catch up a missed run after the machine was off, which cron has no native concept of at all.

For a single systemd-managed host, timers are close to a strict upgrade for jobs that are already services, or that benefit from journal-integrated logging and systemd’s dependency graph.

What they didn’t fix: systemd timers are still a single-host mechanism. A .timer unit tells you about the machine it’s defined on. It has no concept of “this job across your fleet” any more than cron does. You trade crontab syntax for unit-file syntax and gain journal integration, and fleet visibility is unchanged. Migration also costs something: two files per job instead of one line, systemd-specific syntax to learn, and nothing to migrate to on macOS or non-systemd Linux, so a mixed fleet still runs cron somewhere.

What Kubernetes CronJobs changed, and what they didn’t fix

Kubernetes CronJob resources reached general availability in Kubernetes 1.21, released April 2021. The API had existed since 1.4 and spent a long stretch in beta before that (Kubernetes blog: CronJob Reaches GA). A CronJob spec differs from a crontab line in a way that matters: each scheduled run is a Job resource with its own pod, its own resource requests and limits, its own restart policy, and, because it inherits everything else Kubernetes already does, the same scheduling, isolation, and observability primitives as any other workload in the cluster. concurrencyPolicy gives you Forbid/Replace/Allow semantics for overlapping runs declaratively, instead of a hand-rolled flock wrapper.

For teams already on Kubernetes, this consolidates things: one system decides where scheduled work runs, enforces its resource limits, and governs who can change it through one set of RBAC rules.

CronJobs only exist inside a cluster. They tell you nothing about the bare-metal box running your database backups, a developer laptop with a local sync job, or a vendor-managed server you reach only over SSH. Most organizations of any size still have non-Kubernetes machines like the bastion host, the on-prem appliance, or the one server nobody has containerized, and a CronJob dashboard shows none of them.

The rise of managed and serverless schedulers

The other branch of the family tree is hosted: cloud-provider cron-as-a-service (EventBridge Scheduler, Cloud Scheduler, and equivalents) that trigger a function or a queue message on a schedule instead of running a script on a machine you manage, and third-party monitoring layers that sit on top of your existing cron/systemd/CronJob setup rather than replacing it, pinging a URL on every run and alerting when the ping doesn’t arrive on schedule.

Cloud-provider schedulers work differently from cron. There’s no host to keep patched, no daemon to confirm is running, no crontab to lose track of across machines. The schedule is a resource in your cloud account, and the thing it triggers is typically stateless and ephemeral by construction. That fits a specific shape of workload well: a Lambda that resizes images, a Cloud Function that syncs a feed, work that’s naturally short-lived and doesn’t care what host it runs on. It fits badly for anything that needs to run on a particular machine: a backup job that has to read a specific server’s local disk, a script that depends on state accumulated on that host, anything where “which machine” is part of the requirement rather than an implementation detail the platform is supposed to hide from you.

Monitoring layers solve a narrower problem than replacing cron: they show whether a scheduled thing happened, which cron has no native support for. A cron job that silently stops running produces no signal anywhere unless something is explicitly watching for the absence of a heartbeat. Cron doesn’t know its own job failed unless the job’s own exit code says so, and even then, by default, that exit code goes into a mail spool most servers don’t deliver anywhere. A monitoring layer closes exactly that gap: ping an endpoint on every successful run, get alerted the moment a scheduled ping doesn’t arrive.

That’s useful, but passive. It tells you a job didn’t run, and because it has no write access it can’t show or change the failing schedule. The alert says “the 2am backup didn’t run.” Finding out why and fixing it still means SSHing into whichever box the job actually lives on and running crontab -e, which puts you back at the single-host, single-syntax starting point cron gave you. Detection improved, and remediation is unchanged.

What’s still missing

Cron solved single-machine scheduling with almost no ceremony, and never tried to solve anything else. systemd timers solved logging and dependency integration, for systemd hosts only. Kubernetes CronJobs solved workload-level resource isolation and RBAC, for clusters only. Monitoring layers solved after-the-fact visibility, without write access to fix what they observe. Each tool improves on bare cron within its own scope, and none of them looks across the boundary of the system it belongs to.

The durable operational problem is the one none of these tools was designed to solve: most real infrastructure is heterogeneous. A team running Kubernetes in production also has a bastion host, a couple of legacy servers nobody’s migrated, someone’s laptop running a personal sync job, a vendor appliance reachable only by SSH. Scheduled work lives on all of it. There’s still no single place to see every scheduled job across that whole mix, tell which are failing, and change one without SSHing into its box. That’s a control-plane problem, and cron, systemd timers, and Kubernetes CronJobs each leave it as they found it.

FAQ

Is cron actually deprecated in 2026? No. It ships by default on virtually every Linux distribution and macOS, and nothing in active development has formally deprecated it. It remains the correct choice for simple, single-machine scheduling where the operational overhead of an alternative isn’t justified.

Should I replace cron with systemd timers? For jobs already running as systemd services on a systemd-managed host, timers give you journal-integrated logging and dependency ordering that cron doesn’t have natively, which is a real improvement. They don’t help you if the job runs on macOS, a non-systemd Linux, or if you need visibility across more than one machine, since a timer unit is still scoped to the host it’s defined on.

Do Kubernetes CronJobs replace the need for cron? Only for workloads that already run inside the cluster. CronJobs became GA in Kubernetes 1.21 (April 2021) and give you resource isolation, RBAC, and declarative concurrency handling for jobs Kubernetes already schedules, but they have no visibility into anything running outside the cluster.

What actually solves fleet-wide cron visibility? Nothing built into cron, systemd, or Kubernetes does. Each is scoped to its own host or cluster by design. A control-plane layer on top of your existing schedulers is meant to close that gap by letting you see and manage scheduled jobs across every machine while the schedulers stay in place.


norc works alongside cron, systemd timers, and Kubernetes CronJobs. It shows scheduled cron jobs and captured results for managed cron entries on local machines and remote servers. See how it’s positioned against monitoring-only tools on /compare.

Request an invite

Leave your email and we'll send an invite when a spot opens.