Skip to main content

Apache Airflow: DAG Orchestration, Setup & Best Practices

Apache Airflow is the industry-standard open-source platform for authoring, scheduling, and monitoring data pipelines as code. Define your workflows as Python DAGs and get full visibility into every task run, retry, and dependency.

✓ Pipelines as Python code ✓ Dynamic DAG generation ✓ Rich operator ecosystem ✓ Built-in retry & alerting

What is Apache Airflow?

Apache Airflow is a platform to programmatically author, schedule, and monitor workflows. Originally created at Airbnb in 2014 and donated to the Apache Software Foundation in 2016, it has become the dominant open-source workflow orchestration tool for data engineering teams worldwide. Airflow represents pipelines as Directed Acyclic Graphs (DAGs) written in Python, which means pipelines can be dynamically generated, version-controlled in Git, and tested like any other code.

The Airflow scheduler continuously monitors all DAGs, triggers task instances when their dependencies are met, and distributes work to an executor. The web UI provides a graphical view of DAG runs, task logs, dependency graphs, and Gantt charts, making it straightforward to diagnose failures and monitor pipeline health in production.

Core Concepts

DAG Definition with Operators & Sensors

# dags/etl_pipeline.py
from datetime import datetime, timedelta
from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.operators.bash import BashOperator
from airflow.providers.http.sensors.http import HttpSensor

default_args = {
    'owner': 'data-team',
    'retries': 2,
    'retry_delay': timedelta(minutes=5),
    'email_on_failure': True,
}

def extract_data(**context):
    import requests
    response = requests.get('https://api.example.com/data')
    context['ti'].xcom_push(key='raw_data', value=response.json())

def transform_data(**context):
    raw = context['ti'].xcom_pull(key='raw_data', task_ids='extract')
    transformed = [{'id': r['id'], 'value': r['v'] * 2} for r in raw]
    context['ti'].xcom_push(key='transformed', value=transformed)

with DAG(
    dag_id='etl_pipeline',
    default_args=default_args,
    schedule_interval='@daily',
    start_date=datetime(2024, 1, 1),
    catchup=False,
    tags=['etl', 'production'],
) as dag:

    check_api = HttpSensor(
        task_id='check_api',
        http_conn_id='api_default',
        endpoint='/health',
        timeout=20,
        poke_interval=5,
    )

    extract = PythonOperator(
        task_id='extract',
        python_callable=extract_data,
    )

    transform = PythonOperator(
        task_id='transform',
        python_callable=transform_data,
    )

    load = BashOperator(
        task_id='load',
        bash_command='python /opt/scripts/load_to_warehouse.py',
    )

    check_api >> extract >> transform >> load

Docker Compose Quickstart

# docker-compose.yml (Airflow quickstart)
version: '3.8'
x-airflow-common: &airflow-common
  image: apache/airflow:2.9.0
  environment:
    AIRFLOW__CORE__EXECUTOR: LocalExecutor
    AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres/airflow
    AIRFLOW__CORE__FERNET_KEY: ''
    AIRFLOW__CORE__DAGS_ARE_PAUSED_AT_CREATION: 'true'
    AIRFLOW__API__AUTH_BACKENDS: airflow.api.auth.backend.basic_auth
  volumes:
    - ./dags:/opt/airflow/dags
    - ./logs:/opt/airflow/logs
    - ./plugins:/opt/airflow/plugins
  depends_on:
    postgres:
      condition: service_healthy

services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_USER: airflow
      POSTGRES_PASSWORD: airflow
      POSTGRES_DB: airflow
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "airflow"]
      interval: 5s

  airflow-webserver:
    <<: *airflow-common
    command: webserver
    ports:
      - "8080:8080"

  airflow-scheduler:
    <<: *airflow-common
    command: scheduler

  airflow-init:
    <<: *airflow-common
    command: version
    entrypoint: /bin/bash
    environment:
      _AIRFLOW_DB_UPGRADE: 'true'
      _AIRFLOW_WWW_USER_CREATE: 'true'
      _AIRFLOW_WWW_USER_USERNAME: admin
      _AIRFLOW_WWW_USER_PASSWORD: admin

XComs, Variables & Connections

# XComs and Connections
from airflow.models import Variable
from airflow.hooks.base import BaseHook

# Read Airflow Variable (encrypted in metastore)
api_key = Variable.get('my_api_key', deserialize_json=False)

# Read a Connection
conn = BaseHook.get_connection('my_postgres')
print(conn.host, conn.schema, conn.login)

# Push/pull XCom values between tasks
def producer(**context):
    context['ti'].xcom_push(key='result', value=42)

def consumer(**context):
    val = context['ti'].xcom_pull(
        task_ids='producer_task',
        key='result'
    )
    print(f"Got: {val}")  # 42

Airflow vs Prefect vs Dagster

Feature Apache Airflow Prefect Dagster
Paradigm Task DAG (push) Flow/Task (push) Asset-based (pull)
Language Python Python Python
UI Webserver (self-hosted) Prefect Cloud / self-hosted Dagit (self-hosted)
Dynamic workflows Limited (requires workarounds) Native Native (partitions)
Data lineage Plugin required Limited First-class (Assets)
Learning curve Medium Low–Medium Medium–High
Managed cloud Astronomer / MWAA / Cloud Composer Prefect Cloud Dagster Cloud
Best for Complex ETL, large teams Modern Python teams Data platform / lineage

Best Practices

Frequently Asked Questions

What is a DAG in Apache Airflow?▼

A DAG (Directed Acyclic Graph) is a Python file that defines a workflow as a set of tasks and their dependencies. 'Directed' means data flows in one direction, 'Acyclic' means no circular dependencies. Each task in a DAG is an instance of an Operator (BashOperator, PythonOperator, etc.). Airflow's scheduler reads DAG files from the dags/ directory and schedules task runs based on the defined schedule_interval and start_date.

What is the difference between Airflow Executors?▼

The SequentialExecutor runs one task at a time (only for development). The LocalExecutor uses multiprocessing to run tasks in parallel on the same machine — suitable for small to medium workloads. The CeleryExecutor distributes tasks across a cluster of worker nodes using a message broker (Redis or RabbitMQ) — suited for large-scale production. The KubernetesExecutor spins up a fresh Pod per task in Kubernetes — provides strong isolation and resource control. Most production deployments use Celery or Kubernetes executors.

How do XComs work in Airflow?▼

XComs (Cross-Communications) allow tasks to exchange small pieces of data via Airflow's metadata database. A task pushes a value with ti.xcom_push(key, value) and a downstream task pulls it with ti.xcom_pull(task_ids, key). XComs are stored in the database so they are size-limited — keep them small (IDs, filenames, counts). For large datasets, write to an intermediate storage (S3, GCS, local disk) and pass only the path via XCom. In Airflow 2.x, PythonOperator automatically pushes the function return value as an XCom.

What is the catchup parameter in a DAG?▼

When catchup=True (the default), Airflow will backfill all DAG runs from the start_date to the current date when the DAG is first enabled. For a daily DAG with a start_date six months ago, this triggers 180 immediate runs. Setting catchup=False prevents backfilling — only the next scheduled run will execute. Unless you need historical backfills, always set catchup=False in production DAGs to avoid overwhelming your infrastructure when deploying or re-enabling a paused DAG.

How do Airflow Connections differ from Variables?▼

Connections store credentials for external systems (databases, APIs, cloud services) — they include fields for host, port, schema, login, password, and extra JSON config. They are managed via the Airflow UI under Admin > Connections or via environment variables (AIRFLOW_CONN_MY_CONN=...). Variables store arbitrary key-value configuration, optionally serialized as JSON. Variables are accessible anywhere in a DAG with Variable.get(). Both can be encrypted at rest using Fernet encryption configured via AIRFLOW__CORE__FERNET_KEY.

When should I use Airflow vs a simpler cron-based approach?▼

Use cron for simple, independent jobs with no inter-task dependencies and no need for retries, monitoring, or backfilling. Use Airflow when you have multi-step pipelines where later tasks depend on earlier ones, need automatic retries on failure, require centralized scheduling and visibility across many pipelines, need backfilling capability, or work across a team where visibility and auditability matter. The operational overhead of running Airflow (metadata database, scheduler, webserver) is not justified for one or two simple scripts.

Share this tool: X / Twitter LinkedIn
✓ 60,000+ developers use DevPlaybook ★★★★★ 4.8 / 5 on Gumroad ✓ MIT licensed · Instant download ✓ No login required

Related Products

on Gumroad

You Might Also Need

More Free Tools

Enjoying this tool? Get more like it.

Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.

DevPlaybook Pro 7 days free

Unlock AI-Powered Dev Tools

  • ⚡ AI Code Review, Doc Generator & SQL Builder
  • ⚡ All premium templates & early access
  • ⚡ Member discounts on Gumroad products
Start Free Trial →

$9/mo after trial · Cancel anytime

Want the full toolkit?

Get DevPlaybook Pro

Every template, guide, boilerplate, and automation script in one bundle. 13 premium products — grab them all at once and save big.

$79
$241 separately Save $162 (67% off)
Get DevPlaybook Pro — $79 →

MIT licensed · Instant download · No subscription

See what's included → Browse all deals →