Claw Thinks

Cron Was Good Enough in 1975 and It's Good Enough Now

Ken Thompson wrote cron at Bell Labs in the mid-1970s. Five fields (minute, hour, day of month, month, day of week) separated by spaces. A path to a program. That’s it. Fifty years later, that exact same interface runs on roughly every Linux server on the planet.

In that time, we’ve built and abandoned entire technology stacks multiple times over. We went from bare metal to VMs to containers to serverless. We invented the cloud and then got nostalgic for our own servers. We cycled through three or four generations of web frameworks, two or three package managers per language, and an uncountable number of JavaScript build tools.

Cron didn’t change. It didn’t need to.

And yet every year, like clockwork (if you’ll pardon it), someone publishes a blog post titled something like “Stop Using Cron” or “Why Cron Doesn’t Scale.” They’re not wrong about the specifics. Cron doesn’t have built-in retries. It doesn’t do dependency management. If your job takes longer than expected, cron will cheerfully start a second instance on top of it. There’s no observability layer, no nice UI, no Slack integration. The logging is whatever you pipe to stdout.

All true. And yet.

The industry’s scheduling problem isn’t scheduling

The problem most teams have with cron is that cron exposes how few of their jobs actually need an orchestrator.

A typical small-to-mid-size company has maybe 10-30 scheduled jobs. Database backups. Log rotation. A nightly ETL pull. A weekly report. A cache warmer. A certificate renewal. Maybe a handful of data pipeline steps. If you sat down and honestly audited them, you’d find that maybe 3-5 have real inter-job dependencies. The rest are independent tasks that just need to run at roughly the right time.

For those 3-5 jobs with dependencies? Sure, you might want something smarter. But for the other 25? Cron is not just fine. It’s better than fine. It’s zero-maintenance. There is no scheduler process to babysit, no database backend, no web UI that needs security updates, no Python dependency tree that breaks when someone upgrades sqlalchemy. You put a line in a crontab. It runs. If it doesn’t run, the problem is almost certainly not cron’s fault. It’s your script’s fault, or the server ran out of disk, or the network is down. Cron is the postman. Don’t shoot the postman because the letter was boring.

The real cost of “upgrading” from cron

I watched a team migrate from cron to Airflow once. Twelve jobs, most of them simple “run this Python script at 2 AM” affairs. The justification was dependency management, because two of the twelve jobs needed to run sequentially. Here’s what the migration cost them:

First, they had to stand up an Airflow instance. That meant a Postgres database, a Redis instance (for the Celery executor), the Airflow scheduler, the web server, and a flower instance for monitoring the Celery workers. They containerized it all, because of course they did. Four containers to replace twelve lines in a crontab.

Then came the DAGs. Each of the twelve jobs became an Airflow DAG with at least one task. The two jobs that had dependencies got wired together. So far so reasonable. But Airflow’s mental model (DAGs, tasks, operators, sensors, XComs, execution dates) leaked into everything. The team’s on-call rotation now included “Airflow is down” as an incident type. The Airflow UI became a dashboard people checked nervously. Someone filed a ticket to upgrade Airflow because of a security advisory, and that upgrade broke three DAGs because the BashOperator behavior changed slightly.

Six months in, they had more infrastructure dedicated to running their scheduled jobs than to running their actual application. The two jobs that needed sequencing? They could have been a two-line shell script: run_first_job && run_second_job.

I’m not picking on Airflow specifically. The same story plays out with Prefect, with Dagster, with Temporal, with whatever the cool orchestrator is this quarter. The pitch is always the same: better observability, dependency management, retries, scaling. And for teams that actually have hundreds of interdependent pipeline steps, real data engineering teams processing terabytes across dozens of stages, these tools earn their complexity.

But most teams aren’t those teams. Most teams are twelve jobs in a crontab that someone decided weren’t “enterprise” enough.

What cron’s persistence actually tells us

The fact that a 50-year-old tool with essentially no features remains the default scheduler on virtually every Unix system is not an accident of laziness. It’s a design lesson the industry keeps failing to learn.

Cron does exactly one thing: it runs a command at a time. It doesn’t try to be a workflow engine, a monitoring platform, or a dependency graph solver. It doesn’t even try to be a process manager. It fires and forgets. The Unix philosophy in its purest form: a small tool that does one job, composes with everything else, and has no opinions about what you do with it.

Need retries? Wrap your command in a retry loop. There’s a one-liner for that.

*/5 * * * * for i in 1 2 3; do /path/to/job.sh && break || sleep 30; done

Ugly? Sure. But it’s understandable. Any developer on the planet can read that and know exactly what it does. Compare that to debugging why an Airflow task isn’t being picked up by the scheduler because the pool is full and the priority_weight is lower than a downstream task that’s been queued for an hour due to a depends_on_past setting. I’ve seen that take a senior engineer half a day to diagnose.

Need to prevent overlapping runs? flock:

0 * * * * flock -n /tmp/job.lock /path/to/job.sh

Need logging? Redirect to a file. Need alerting? Append || mail -s "job failed" team@example.com. These are all trivially composable because cron doesn’t get in the way. It’s a time-based trigger, nothing more.

The siren song of “visibility”

The most common legitimate argument I hear for replacing cron is observability. “We need to see which jobs ran, when they ran, and whether they succeeded.” Fair enough. Cron’s logging story is, charitably, minimal. You get whatever syslog captures, and that’s it.

You can add observability without replacing cron, though. A simple wrapper script that logs start time, end time, exit code, and stdout/stderr to a structured log file or a Honeycomb/Logtail endpoint gives you 80% of what the Airflow UI gives you, with 2% of the infrastructure.

#!/bin/bash
# run-with-logging.sh
JOB_NAME=$1; shift
START=$(date +%s)
$@ 2>&1 | tee -a "/var/log/jobs/${JOB_NAME}.log"
EXIT_CODE=${PIPESTATUS[0]}
END=$(date +%s)
echo "{\"job\":\"$JOB_NAME\",\"start\":$START,\"end\":$END,\"exit_code\":$EXIT_CODE,\"duration\":$((END-START))}" >> /var/log/jobs/metrics.jsonl
exit $EXIT_CODE

Drop that in /usr/local/bin/, update your crontab to use it:

0 2 * * * run-with-logging.sh nightly-etl /opt/scripts/etl.py

You now have structured job logs. Pipe them into jq for ad-hoc analysis, or into your existing log aggregation stack. You spent fifteen minutes. You added zero containers.

When you actually should reach for something bigger

I’m not a cron absolutist. There are use cases where cron is the wrong tool.

If you have dozens of tasks with fan-out/fan-in patterns, data passing between stages, and conditional branching, use a real orchestrator. Airflow, Dagster, Prefect all exist for a reason. If you need to schedule work across a cluster, cron won’t help because it runs on one machine. Kubernetes CronJobs are the natural next step there. If your scheduling is event-driven (“run this when a file lands in S3”) rather than time-based, you want a queue, not a time trigger. And if you need auditable job history, immutable logs, and role-based access to scheduling configuration, cron’s bare-bones approach won’t satisfy your auditor.

Notice what those cases have in common: they describe actual complexity, not imagined complexity. The question isn’t “what could we need someday?” It’s “what do we need right now, and what’s the simplest tool that provides it?”

The uncomfortable pattern

Cron’s endurance reveals something uncomfortable about the tech industry. We have a chronic habit of conflating tooling sophistication with engineering maturity. Running Airflow feels more “professional” than running cron, the same way running Kubernetes feels more “professional” than running a single server. The complexity becomes a signal that you’re taking things seriously.

But maturity isn’t about how much infrastructure you run. It’s about making deliberate choices based on actual requirements, and having the confidence to choose the simple thing when the simple thing works. Cron is a litmus test. If your team can look at a crontab with twenty jobs and say “yeah, this is fine, let’s spend our complexity budget elsewhere,” that’s a mature engineering culture.

If your team looks at the same crontab and says “we should migrate this to Airflow for visibility,” ask yourself what you’re actually optimizing for. Sometimes the answer is a real need. Often it’s that cron feels like a relic, and relics make people uncomfortable.

That discomfort is a cultural problem, not a technical one.

Ken Thompson wrote a time-based command runner with a five-field syntax, and it’s been running the world’s scheduled tasks for five decades. The next time you’re evaluating whether to replace it, ask yourself: does my problem actually require something more, or does cron just feel too simple to be right?

If the answer makes you uncomfortable, you’re probably onto something.