Matthew Kassel
Back to Blog Circuit board close-up representing automated scheduling and data pipeline orchestration
etlpythondata

Airflow vs. Cron: When Do You Actually Need a Real Scheduler?

Cron gets a bad reputation it doesn't always deserve. Here's an honest breakdown of when cron is the right tool, when Airflow earns its complexity, and what to use in between.

Every data engineer eventually faces the scheduler question. You’ve got a script that needs to run on a schedule. Do you throw it in cron and move on, or is it time to set up Airflow?

The answer I usually get from senior engineers: “just use Airflow, you’ll need it eventually anyway.” The answer I actually give: it depends, and a lot of teams are running Airflow when they’d be better served by something much simpler.

Let me break this down honestly.

Cron Is Not the Enemy

Cron has been orchestrating scheduled tasks since 1975 and it does its job reliably. For a lot of data work, it’s genuinely the right tool. Here’s when I reach for cron without apology:

Use cron when:

  • You have one to five scripts that run independently of each other
  • Failure handling is simple (retry logic in the script itself, email on error)
  • The schedule doesn’t change often
  • You’re on a small team and nobody needs a UI to see what’s running
  • The scripts finish in under a few minutes each
# A perfectly reasonable crontab
0 6 * * * /usr/bin/python3 /home/matthew/scripts/sync_orders.py >> /var/log/sync_orders.log 2>&1
30 6 * * * /usr/bin/python3 /home/matthew/scripts/enrich_leads.py >> /var/log/enrich_leads.log 2>&1
0 8 * * 1 /usr/bin/python3 /home/matthew/scripts/weekly_report.py >> /var/log/weekly_report.log 2>&1

Three jobs, three log files, done. This runs on a $5/month VPS and has exactly zero operational overhead. For a small operation, this is not embarrassing — it’s appropriate.

The cron haters tend to be people at large companies with dozens of interdependent pipelines. They’re projecting their complexity onto your situation.

Where Cron Falls Down

That said, cron has real limits that sneak up on you:

Dependencies. Cron has no concept of “run this only after that finished successfully.” You work around it with sleep timers and blind hope. The moment you have a pipeline where step B depends on step A completing cleanly, you’re fighting cron instead of working with it.

Visibility. Failed jobs send you an email if you configured it, or silently disappear if you didn’t. There’s no dashboard, no history, no way to see “what ran yesterday and what didn’t.” When something goes wrong at 3 AM, you’re reading log files.

Backfill. Need to re-run last Tuesday’s job with last Tuesday’s parameters? Good luck. Cron doesn’t know what day it was when it ran. Your script better handle that logic itself.

Parallelism and resource management. Cron will happily start the same job twice if the first run is still going. At best, this wastes resources. At worst, it creates duplicate data.

Dynamic pipelines. If the set of tasks you need to run changes based on data (process one task per client, or one task per file in a directory), cron gets awkward fast.

What Airflow Actually Gives You

Apache Airflow is a pipeline orchestration platform. You define your pipelines as DAGs (Directed Acyclic Graphs) in Python — each node is a task, each edge is a dependency. The scheduler respects those dependencies, manages retries, maintains a history of every run, and gives you a UI to monitor everything.

The genuine wins:

Dependency management — task B will not run until task A succeeds. Task C runs after both B and D. This is Airflow’s core superpower and it’s worth a lot.

Visibility — the Airflow UI shows you every DAG run, every task, its status, its logs, how long it took. When something breaks, you can see exactly where in the pipeline it broke.

Backfill — you can trigger a backfill for any historical date range and Airflow will run the pipeline for each interval, in order, with the correct execution date. This is invaluable.

Retry configuration — define retry behavior at the task level. Three retries with five-minute delays? Done. Exponential backoff? Done.

Sensors — wait for a file to appear, an API to return data, or another DAG to finish before proceeding.

from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime, timedelta

default_args = {
    "owner": "matthew",
    "retries": 3,
    "retry_delay": timedelta(minutes=5),
    "email_on_failure": True,
    "email": ["[email protected]"],
}

with DAG(
    "daily_lead_pipeline",
    default_args=default_args,
    schedule_interval="0 6 * * *",
    start_date=datetime(2026, 1, 1),
    catchup=False,
) as dag:

    extract = PythonOperator(
        task_id="extract_from_crm",
        python_callable=extract_crm_data,
    )

    enrich = PythonOperator(
        task_id="enrich_leads",
        python_callable=enrich_lead_data,
    )

    load = PythonOperator(
        task_id="load_to_warehouse",
        python_callable=load_to_bigquery,
    )

    extract >> enrich >> load  # Dependencies defined here

Clean, readable, and the scheduler handles everything else.

The Real Cost of Airflow

Here’s what the Airflow advocates don’t always tell you:

Operational overhead is real. Airflow needs a metadata database (Postgres or MySQL), a web server, a scheduler process, and workers. In production you’re talking about multiple services, proper monitoring of the orchestration layer itself, and someone who knows how to diagnose it when it breaks. On managed services like Astronomer or MWAA, you pay for that simplicity.

The learning curve is non-trivial. DAGs, operators, hooks, XComs, executors, pools — there’s a lot of Airflow-specific vocabulary to learn before you’re productive.

Python all the way down. Your pipeline code is Python. Your DAG definitions are Python. Your custom operators are Python. That’s fine if Python is your world. If your team is primarily SQL or another language, it can be friction.

It’s easy to over-engineer. Airflow makes complex things possible, which tempts engineers to build complex things when simple things would work. I’ve seen three-task pipelines wrapped in elaborate DAGs with custom operators and Slack notifications and branching logic that would have been 50 lines of straightforward Python with a cron entry.

The Middle Ground

Between raw cron and full Airflow, there’s useful territory:

Prefect — Python-native orchestration, much lighter operational footprint than Airflow, good UI, serverless execution options. My default recommendation for teams that need more than cron but aren’t ready to commit to Airflow’s infrastructure.

GitHub Actions / scheduled workflows — surprisingly good for data pipelines. Version-controlled, free for many use cases, integrates with your existing repo. Underused outside of CI/CD contexts.

Dagster — asset-centric rather than task-centric, which is a genuinely different mental model. Opinionated in useful ways. Worth evaluating if you’re building data assets rather than just running jobs.

Simple queue + worker — if your problem is “run many small jobs reliably,” a Redis queue and a few workers (Celery, RQ, or even just threads) is often cleaner than a full orchestration platform.

My Decision Framework

When someone asks me what scheduler to use, I walk through this:

Do you have more than ~10 scheduled jobs?
  No → cron is probably fine
  Yes ↓

Do any jobs depend on other jobs completing first?
  No → cron + better logging is probably fine
  Yes ↓

Do you need historical backfill or run-history visibility?
  No → maybe Prefect or GitHub Actions
  Yes ↓

Do you have the team capacity to operate Airflow?
  No → managed Airflow (MWAA, Astronomer) or Prefect Cloud
  Yes → Airflow

The honest answer for most small data teams: start with cron, add proper logging and alerting, and graduate to Prefect or managed Airflow when you genuinely hit the limits. Don’t pre-optimize for scale you don’t have yet.

The goal is pipelines that run reliably and you can debug quickly. A well-monitored cron job beats a poorly-understood Airflow DAG every time.

— Matthew