How Infra Teams Actually Schedule Work in 2026
Cron is over fifty years old. It has been formally deprecated by nothing, replaced by several things, and is still the mechanism most engineers reach for first when they need something to run at 3am. That persistence is worth taking seriously rather than treating as a curiosity — it’s evidence about what the scheduling problem actually is, and it explains why every well-engineered replacement solved part of it and left the rest standing.
This isn’t a survey. There’s no dataset behind it, no respondent count, no percentage of teams doing anything — anyone citing a specific “X% of teams still use cron” figure should show you where it came from, because there isn’t a reliable source for that number, and this piece won’t invent one. What follows is an argument from how each of these systems is actually built, and from what’s publicly documented about them.
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.
That’s not nothing. A huge fraction of scheduling needs are genuinely simple: run this script nightly, rotate this log weekly, ping this endpoint every five minutes. Cron is the correct amount of tool for that job, and reaching for anything heavier is often over-engineering relative to the actual requirement. The tools that tried to replace cron outright — as opposed to replacing it for a specific context — mostly failed to displace it for exactly this reason: they added ceremony cron doesn’t have, in exchange for capabilities most single-machine scheduling doesn’t need.
Where cron actually fails isn’t the syntax or the daemon. It’s everything cron was never designed to do: tell you when a job failed, coordinate a job across more than one machine, or give you a single place to see what’s scheduled across a fleet instead of SSHing into each box and running crontab -l. Those are real gaps. They’re just not gaps in the scheduling primitive itself — they’re gaps in the operational layer around it, which is a different problem than “cron’s syntax is bad.”
Where systemd timers genuinely 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 — something 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. That’s a real, verifiable improvement, not a marketing claim.
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’ve traded crontab syntax for unit-file syntax and gained journal integration, but the fleet-visibility problem is exactly where it was before. And the migration cost is real: two files per job instead of one line, systemd-specific syntax to learn, and — critically — nothing to migrate to on macOS or any non-systemd Linux, so a mixed fleet ends up running cron somewhere no matter what you pick for the systemd hosts.
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 is genuinely different 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 running Kubernetes, this is a real consolidation: one system decides where scheduled work runs, one system enforces its resource limits, one set of RBAC rules governs who can change it. That’s not a small thing operationally.
What it didn’t fix, and arguably made more visible: CronJobs only exist inside a cluster. They tell you nothing about the bare-metal box running your database backups, the developer laptop with a local sync job, or the vendor-managed server you reach only over SSH. Kubernetes solved fleet-wide scheduling for the fleet that is Kubernetes, which is a large and growing slice of infrastructure but not the whole of it — and most organizations of any size still have real, unglamorous non-Kubernetes surface: the bastion host, the on-prem appliance, the one server nobody’s gotten around to containerizing. A CronJob dashboard shows you the cluster. It has nothing to say about anything outside it.
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.
The cloud-provider schedulers are a genuinely different model, not just cron with a UI. 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 and more honest problem than “replace cron”: they solve visibility into whether a scheduled thing happened, which cron genuinely has zero 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 valuable and easy to underrate. It’s also passive by construction — it tells you a job didn’t run; it doesn’t let you look at or change the schedule that’s failing, because it was never given write access to it in the first place. 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 exactly the single-host, single-syntax starting point cron gave you five decades ago. Detection improved. Remediation didn’t move at all.
The problem none of them solved
Line them up and the pattern is consistent. 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 is a real, defensible improvement over bare cron within its own scope. None of them is a fleet-wide answer, because none of them was built to look across the boundary of the system they’re part of.
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 has never been a single place to see every scheduled job across that whole mix, understand which ones are failing, and change one without SSHing into the specific box it happens to live on. That’s not a syntax problem or a logging problem — it’s a control-plane problem, and it’s the gap cron, systemd timers, and Kubernetes CronJobs each leave exactly where they found it, because solving it was never in scope for any of them individually.
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 — a genuine 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 — 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. That’s the gap a control-plane layer on top of your existing schedulers is meant to close: seeing and managing scheduled jobs across every machine, not replacing the scheduler itself.
norc doesn’t ask you to replace cron, systemd timers, or Kubernetes CronJobs — it’s the layer that shows you what’s scheduled and what failed across all of them, local machines and remote servers alike. See how it’s positioned against monitoring-only tools on /compare.