Home DevOps Apache Airflow Basics: What Breaks When Cron Runs It
Beginner 3 min · September 04, 2026
Introduction to Apache Airflow

Apache Airflow Basics: What Breaks When Cron Runs It

Airflow replaces cron with tracked DAG runs, retries, and a metadata DB.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.

Follow
Production
production tested
September 04, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 25 min
  • Basic Python: functions, imports, pip
  • Comfort running commands in a Linux terminal
  • Know what cron is, even at a rough level
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Airflow is a workflow engine that runs Python-defined DAGs with tracked runs, retries, and dependencies instead of blind cron fires
  • Four runtime pieces coordinate through the metadata DB: scheduler, executor plus workers, API server with webserver UI, and the database itself, plus the triggerer for deferred tasks
  • A DAG file importing in 300ms versus 12s decides whether scheduling stays on time; top-level DB calls stall every scheduler heartbeat
  • Production rule: every load task must be idempotent, so retries and backfills repair history instead of duplicating it
  • Use Airflow for scheduled batch pipelines with dependencies; keep one-liner time triggers on cron
✦ Definition~90s read
What is Introduction to Apache Airflow?

Apache Airflow is an open-source workflow engine where scheduled pipelines are declared as Python DAGs and every run is tracked in a metadata database.

Think of cron as an alarm clock that rings but never checks whether you actually got up, while Airflow is a project manager who assigns every job, checks each one off a list, reassigns failed work, and keeps a written record of everything that happened and when.
Plain-English First

Think of cron as an alarm clock that rings but never checks whether you actually got up, while Airflow is a project manager who assigns every job, checks each one off a list, reassigns failed work, and keeps a written record of everything that happened and when.

A billing cron ran twice in one night and nobody noticed until customers complained. There was no run history, no retry record, no dependency map. Just a syslog line and an invoice table with twice the rows.

That is the cron trap. It fires commands at times but remembers nothing. When a window is missed or a box reboots mid-job, cron cannot tell you what already ran. You are left reconstructing truth from scattered logs.

Airflow exists to end that guessing. It stores every run, retries failures, and won't run downstream work until upstream work succeeds. You'll see how four components deliver that guarantee.

Four pieces. One source of truth. Zero double-bills.

What a Workflow Engine Does That Cron Can't

Cron fires a command at a wall-clock time and forgets it instantly. There is no record of the run, no retry with backoff, and no notion that step B must wait for step A. If the box reboots mid-job, the next fire has no idea the last one half-finished.

A workflow engine adds memory. Every scheduled run becomes a row in a database with a state, a timestamp, and per-task logs. Failed tasks retry on a policy you define, and downstream tasks wait until their upstreams genuinely succeed.

That memory is what turns a script into a pipeline you can trust at 2 AM. You stop asking did it run and start asking which run, which task, and what did the log say.

Top tutorials also frame the career angle: Airflow shows up in 60%+ of data-engineering listings alongside Spark and dbt. You'll run ETL, ML training, and infra automation on the same engine. It's batch-first though — for sub-second streams you'd pick Kafka or Flink, and you can poll every 30 seconds but that's not real eventing.

📊 Production Insight
Cron's blast radius is one box with zero audit trail.
A missed window re-runs silently and bills twice.
Rule: track every money-adjacent run in a database.
🎯 Key Takeaway
Cron fires and forgets; a workflow engine remembers every run.
Memory is the feature: state, retries, and ordering live in one place.
If it is not tracked, it did not happen.

The DAG: Tasks, Edges, Acyclicity

A DAG is a Python file that declares tasks and the edges between them. Tasks do the work: run SQL, call APIs, transform files. Edges declare ordering: this task runs after that one, these two run in parallel.

Acyclic is the load-bearing word. The graph cannot loop back on itself, so the scheduler can always sort tasks into a runnable order. If task A waits on B and B waits on A, Airflow refuses the DAG instead of deadlocking at midnight.

You write the what, Airflow handles the when and the what-if. Dependencies, retries, and parallel fan-out come from the graph structure, not from sleep timers in bash.

dags/billing_daily.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pendulum
from airflow.sdk import dag, task

@dag(
    schedule="@daily",
    start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
    catchup=False,
    tags=["billing", "foundations"],
)
def billing_daily():
    @task(retries=3)
    def charge_customers() -> str:
        # idempotent upsert keyed on invoice_id
        return "2026-09-03"

    @task
    def send_receipts(batch_id: str) -> None:
        print(f"receipts for batch {batch_id}")

    send_receipts(charge_customers())

billing_daily()
📊 Production Insight
Cycles fail at parse time, not at 2 AM.
Fan-out parallelism is free once edges are explicit.
Rule: sketch the graph before writing the file.
🎯 Key Takeaway
Tasks are nodes, edges are ordering, acyclic keeps it schedulable.
Draw the graph before writing code and half your bugs vanish.
No cycles, no midnight deadlocks.

The 4 Components and How They Talk Through the Metadata DB

Four pieces do the work. The scheduler reads DAG files, creates runs, and queues tasks whose dependencies are met. The executor and its workers run the task instances. The API server and webserver render runs, logs, and controls. The metadata DB sits in the middle as shared truth.

Nothing calls anything directly. The scheduler writes task states to the database, workers claim queued tasks and write results back, and the UI reads the same rows. That indirection is why you can restart any component without losing history.

The triggerer deserves a mention too. Deferred tasks like long-polling sensors park there instead of burning worker slots. It is the reason a 40-minute wait costs nearly nothing in Airflow 3.x.

Pick the executor early. SequentialExecutor is dev-only, LocalExecutor gives single-box parallelism, CeleryExecutor and KubernetesExecutor scale to fleets. The DAG processor parses files and the scheduler creates runs every minute — that's why airflow db migrate (3.x replacement for db init) and one shared Postgres matter before any DAG runs.

check_components.shBASH
1
2
3
4
5
# every component reads and writes the same metadata DB
AIRFLOW_HOME=~/airflow airflow db check
AIRFLOW_HOME=~/airflow airflow dags list
AIRFLOW_HOME=~/airflow airflow dags show billing_daily
AIRFLOW_HOME=~/airflow airflow dags list-runs -d billing_daily --limit 10
📊 Production Insight
Split-brain starts with two databases, not two schedulers.
One Postgres for every component, no exceptions.
Rule: verify db check from each host first.
🎯 Key Takeaway
Scheduler plans, executor runs, UI renders, database remembers.
Shared state beats direct calls for restart safety.
Kill any component; history survives.

Idempotency as the First Law

Idempotency means running twice produces the same result as running once. An upsert keyed on invoice_id is idempotent. A blind INSERT of the same rows is not. Every retry, backfill, and manual rerun in Airflow assumes your tasks honor this contract.

The double-billing incident was an idempotency failure wearing a cron costume. The second execution was survivable; the non-idempotent INSERT was not. Had the load been an upsert, the duplicate run would have been a shrug instead of a refund campaign.

Make it a habit now. Key every write on a stable id, partition overwrites by date, and test by clearing a task and re-running it. Row counts must not move.

⚠ Retries Will Happen to You
A retried task is not an edge case. Transient DB blips, OOM kills, and deploys mid-run make retries weekly events. Design for the second run before you celebrate the first.
📊 Production Insight
Non-idempotent loads turn every retry into corruption.
Clear-and-rerun is the five-minute proof.
Rule: row counts must not move on rerun.
🎯 Key Takeaway
Retries are certain, so re-runs must be harmless by design.
Key writes on stable ids and test with a clear-and-rerun.
Safe reruns beat clever scripts.

Airflow's History: Airbnb 2014 to Apache to 3.x

Airflow began inside Airbnb in 2014 as a fix for cron sprawl across data pipelines. It joined the Apache Incubator in 2016 and grew the provider ecosystem that now covers Snowflake, dbt, Kubernetes, and cloud storage.

Airflow 3.x is the current major line. TaskFlow decorators are the default authoring style, the schedule argument replaced schedule_interval, and assets renamed the old dataset triggers. The core promise never changed: tracked runs with atomic tasks.

Knowing the history helps you read old answers. Any tutorial using schedule_interval or execution_date predates 3.x semantics. Translate to schedule and data intervals before copying code.

Airflow 3.x (3.0 April 2025, 3.2.2 May 2026, 2.x EOL April 2026) rebuilt the UI in React, added built-in DAG versioning so code edits don't disturb in-flight runs, renamed Datasets to Assets for event-driven triggers, and locked tasks behind a Task Execution API instead of direct DB access. Imports consolidate to airflow.sdk and schedule replaces schedule_interval.

dags/invoice_upsert.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import pendulum
from airflow.sdk import dag, task
from airflow.providers.postgres.hooks.postgres import PostgresHook

@dag(
    schedule="@daily",
    start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
    catchup=False,
    tags=["billing"],
)
def invoice_upsert():
    @task(retries=3)
    def upsert_invoices(batch_id: str) -> None:
        hook = PostgresHook(postgres_conn_id="billing_db")
        hook.run(
            """
            INSERT INTO invoices (invoice_id, customer_id, amount, batch_id)
            VALUES (%(invoice_id)s, %(customer_id)s, %(amount)s, %(batch_id)s)
            ON CONFLICT (invoice_id) DO UPDATE
            SET amount = EXCLUDED.amount, batch_id = EXCLUDED.batch_id
            """,
            parameters={
                "invoice_id": "inv-88412",
                "customer_id": "cust-2091",
                "amount": 4900,
                "batch_id": batch_id,
            },
        )

    upsert_invoices("2026-09-03")

invoice_upsert()
📊 Production Insight
Copy-pasted 1.x DAGs break on 3.x scheduler semantics.
Version mismatch wastes more hours than syntax errors.
Rule: confirm 3.x docs before adopting snippets.
🎯 Key Takeaway
Airbnb built it for cron sprawl; Apache scaled it to an ecosystem.
Old tutorials use schedule_interval; 3.x uses schedule.
Check the version before copying.

When Airflow Is (and Isn't) the Right Tool

Airflow shines for scheduled batch work with dependencies: nightly ETL, hourly aggregates, weekly reports. If runs need history, retries, and a UI, it earns its operational cost quickly.

It is the wrong tool for event streaming, sub-second latency, or a single cron line that never fails. Kafka and Flink own streams; cron owns trivial timers. Running a five-second health check through a full Airflow stack is theater.

The honest test is state. If you need to answer what ran, what failed, and what happens on rerun, choose Airflow. If those questions never come up, keep the simpler tool.

Versus Prefect and Dagster: Airflow wins on 80k+ orgs, 30M monthly downloads, and hundreds of providers for Snowflake, dbt, and clouds. Prefect feels lighter for Python-first teams, Dagster models assets natively. Choose Airflow when you need proven scheduling, a deep provider bench, and Grid/Graph observability at 2 AM.

📊 Production Insight
Over-adopting Airflow for one-liners burns ops hours.
Under-adopting it for money pipelines burns refunds.
Rule: state questions decide the tool.
🎯 Key Takeaway
Batch pipelines with dependencies: yes, absolutely Airflow.
Streaming and trivial timers: pick the simpler tool.
Need run history? You need Airflow.
● Production incidentPOST-MORTEMseverity: high

The Cron Job That Double-Billed Every Customer

Symptom
Customers reported duplicate charges by 9 AM and the invoice table held 8,400 rows instead of 4,200 across two batches stamped 01:12 and 01:47. The cron log showed exit code 0 both times, which looked like success. Support tickets jumped from 3 to 140 in 6 hours while no dashboard showed the double execution.
Assumption
The team assumed cron plus a careful shell script was enough because it'd run cleanly for 5 months. They believed exit code 0 meant each of the 4,200 customers was billed exactly once and that a timestamp guard would block re-runs. Nobody'd tested a reboot mid-window, so nobody knew cron keeps no record of the first fire.
Root cause
Cron stores no run state, runs no retries with backoff, and tracks no dependencies between steps. When the host rebooted 22 minutes into the window, cron fired billing.sh a second time with no memory of the first pass. The script's plain INSERT wasn't idempotent, so the second pass added a second set of 4,200 invoices with no central run log to consult.
Fix
They rebuilt billing as billing_daily DAG: charge_customers >> send_receipts with retries=3, idempotent upsert ON CONFLICT (invoice_id) DO UPDATE, and run_id stamped into each invoice batch. They proved it with airflow dags test billing_daily 2026-09-03 and airflow tasks clear billing_daily -t charge_customers --yes followed by a rerun showing row counts flat at 4,200. Failures now page on-call instead of billing twice.
Key lesson
  • Cron has no run state, so any reboot or retry is invisible; track every run in the metadata DB.
  • Money-moving tasks must be idempotent first with retries second, or automation multiplies a 4,200-row mistake into 8,400.
  • A workflow engine pays for itself the first time a 2 AM failure pages you with the exact failed task.
Production debug guideFour failure shapes beginners hit in week one, with the exact check that proves each one.4 entries
Symptom · 01
Suspected double execution with no central run record
Fix
List recent runs with airflow dags list-runs -d billing_daily --limit 20 and compare run_ids against your invoice table's batch column. Any invoice batch without a matching succeeded run is untracked. Move the job into a DAG with a stable run_id, then practice airflow dags test billing_daily 2026-09-03 and airflow tasks test billing_daily charge_customers 2026-09-03 before the next window.
Symptom · 02
Steps ran out of order across boxes
Fix
Run airflow dags show billing_daily to print the dependency graph. If two money steps have no edge between them, they can overlap. Add explicit dependencies with extract >> transform >> load so ordering is enforced by the scheduler instead of clock timing.
Symptom · 03
Failures nobody noticed until customers complained
Fix
Check the metadata DB directly: airflow db migrate once after install, then airflow db check, then query dag_run for failed runs in the last 24h. If failed runs exist that nobody saw, wire SMTP or Slack alerts via default_args on_failure_callback so the next failure pages a human.
Symptom · 04
Retry or rerun duplicated data
Fix
Clear one task instance with airflow tasks clear billing_daily -t charge_customers --yes and re-run it. If row counts grow, the task is not idempotent. Rewrite the load as an upsert on invoice_id before enabling retries.
Cron vs Script Loop vs Airflow
CapabilityCronScript loopAirflow
Run state trackingNone, exit code onlyWhatever you codeEvery run and task stored in metadata DB
Retries with backoffNone, you write wrappersManual try/exceptBuilt-in retries, retry_delay, alerts
DependenciesTime offsets and prayerHardcoded orderExplicit edges, parallel branches, sensors
ParallelismOverlapping processesThreads you manageExecutors scale to hundreds of workers
ObservabilitySyslog and hopePrint statementsGrid, Graph, logs per task, SLA alerts
BackfillManual date loopsCustom rerun scriptsCatchup runs plus airflow dags backfill
Event triggersPolling hacksCustom codeAssets fire downstream DAGs on update
Version safetyCopy the scriptCopy the scriptDAG versioning pins code per run
⚙ Quick Reference
3 commands from this guide
FileCommand / CodePurpose
dagsbilling_daily.pyfrom airflow.sdk import dag, taskThe DAG
check_components.shAIRFLOW_HOME=~/airflow airflow db checkThe 4 Components and How They Talk Through the Metadata DB
dagsinvoice_upsert.pyfrom airflow.sdk import dag, taskAirflow's History

Key takeaways

1
Cron has no run state, so missed windows silently re-run
Airflow stores every run and task in Postgres you'd query in seconds.
2
A DAG turns scripts into tracked runs
tasks, edges, retries, and per-task logs in one Grid/Graph UI you'd trust at 2 AM.
3
Scheduler, DAG processor, executor plus workers, API server, and metadata DB coordinate through the database; 3.x adds versioning and Assets.
4
Idempotency is the first law
upsert on stable keys so retries and backfills repair instead of duplicating — test with clear-and-rerun.
5
Airflow fits scheduled batch with dependencies and 30M-download provider depth; it isn't a streaming engine or a one-liner cron swap.

Common mistakes to avoid

4 patterns
×

Running billing or money-moving jobs from raw cron with no run-state tracking

Symptom
A missed window silently re-runs and double-bills customers; logs exist on one box but nobody can tell which runs already completed.
Fix
Split recurring work into a DAG with one task per step, explicit dependencies, retries, and idempotent writes. Keep cron only for firing airflow dags trigger on rare legacy boxes.
×

Pointing the scheduler and webserver at different metadata databases

Symptom
The UI shows DAGs as unrun while the scheduler log claims success; task history disagrees between screens.
Fix
Route every component through the same Postgres metadata DB (run airflow db migrate once), one connection string. Verify with airflow db check from each host before starting api-server, scheduler, dag-processor, and triggerer.
×

Writing non-idempotent tasks that append on every run

Symptom
Retries and backfills duplicate rows; rerunning a failed DAG doubles data instead of repairing it.
Fix
Design each task to be safely re-runnable: upsert on a stable key or partition, never blind append. Test by clearing a task instance and re-running it.
×

Putting network calls and heavy work at DAG file top level

Symptom
Scheduler parse times climb past seconds per file; new runs start late even though workers sit idle.
Fix
Keep DAG files to DAG structure plus light imports. Move DB calls, API calls, and heavy imports inside task functions so parsing stays under a second.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Why does cron fail for billing pipelines, and what does a workflow engin...
Q02SENIOR
Name Airflow's runtime pieces and how they talk to each other.
Q03SENIOR
How do idempotency and data intervals make backfills safe?
Q01 of 03JUNIOR

Why does cron fail for billing pipelines, and what does a workflow engine add?

ANSWER
Cron launches commands at times with no memory: no run state, no retries, no dependency tracking, and logs scattered per box. A workflow engine stores every run in a metadata DB, retries failed tasks with backoff, blocks downstream tasks until upstreams succeed, and exposes per-task history. That is why a missed cron window can silently double-bill while Airflow would mark the run failed and page you.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is an Airflow DAG in one paragraph?
02
How is Airflow different from cron?
03
Can I run Airflow on one machine?
04
Why does the scheduler read my DAG files repeatedly?
05
What does idempotent mean for a data task?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
September 04, 2026
last updated
1,750
articles · all by Naren
🔥

That's Airflow. Mark it forged?

3 min read · try the examples if you haven't

Previous
Jenkins Build Triggers
1 / 37 · Airflow
Next
Airflow Installation and Setup