Skip to content
Insights/Architecture/Why We Migrated from Airflow to Kubernetes-Native Orchestration
ArchitectureAirflowArgoKubernetes

Why We Migrated from Airflow to Kubernetes-Native Orchestration

After three years running Airflow at scale, we hit the ceiling: resource contention, slow DAG parsing, and a scheduler that became a single point of failure. Here's the full story of how we rebuilt orchestration on Argo Workflows — what we gained, what we lost, and the lessons you can steal.

For three years our orchestration ran on a single t3.xlarge Airflow scheduler with 400 DAGs and 12,000 daily task runs. It worked — until it didn't. By Q3 2025 we were paging the on-call every other night for scheduler heartbeat misses, and the data team was rewriting DAGs to avoid the scheduler instead of solving real problems. This is the story of why we migrated to Argo Workflows, what we gained, what we lost, and the migration protocol we'd repeat.

The breaking point

Our Airflow setup served us well for the first two years. A managed Cloud Composer instance, 400 DAGs, and a team of 12 data engineers. We followed the docs, used KubernetesPodOperator for everything heavy, and kept the DAG files small. The platform team did their job — we never thought about it.

Then the numbers started telling a different story. Pipeline freshness slipped from "by 7am" to "by 10am, usually." On-call pages climbed from one every two weeks to one every other night. By Q3 2025, we had stopped writing new DAGs in patterns the docs recommended, and started writing DAGs to avoid making the scheduler unhappy.

MetricAirflow (Cloud Composer)After migration (Argo)
DAG parse time47–55 secondsEliminated
Scheduler heartbeat lag8–22 s< 200 ms
Task start latency45–90 seconds4–12 seconds
Slot contentionEntire schedulerPer-workflow only
Infra + ops cost~$4,200/mo~$960/mo

A single misconfigured DAG could bring down the entire scheduler. That was the final straw — one team's bad dynamic-DAG generator stalled the whole company's pipelines for 90 minutes. We started the migration evaluation the next morning.

The architecture problem

WHAT WE HAD · AIRFLOW WHAT WE BUILT · ARGO Scheduler (single binary) Executor → Celery workers Postgres (metadata, locks) Web UI + Flower Argo Controller (HA) Workflow CRDs (native k8s) Argo Events → triggers Argo UI + Prometheus
Side-by-side: Airflow components vs Kubernetes-native equivalents

We were already using KubernetesPodOperator for every meaningful task — every task was a pod. Airflow had become a thin, expensive scheduling wrapper around Kubernetes, maintaining its own database, scheduler process, web server, and message broker for no incremental value.

Argo Workflows runs entirely as Kubernetes CRDs. No separate database, no scheduler process — the Kubernetes control plane is the orchestration layer. The Argo controller is stateless and horizontally scalable; it watches for Workflow resources and reconciles them. That's it.

Side-by-side: Airflow vs Argo

The same pipeline in both systems shows exactly where the complexity lives.

Pythonairflow_kpo.py// Airflow KubernetesPodOperator
from airflow.providers.cncf.kubernetes.operators.kubernetes_pod import KubernetesPodOperator
from kubernetes.client import models as k8s

transform_task = KubernetesPodOperator(
    task_id="transform_events",
    namespace="dataeng",
    image="registry/transform:2.1",
    cmds=["python"],
    arguments=["/app/transform.py"],
    env_vars=[k8s.V1EnvVar(name="DATE", value="{{ ds }}")],
    resources=k8s.V1ResourceRequirements(
        requests={"cpu": "500m", "memory": "1Gi"},
        limits={"cpu": "2",     "memory": "4Gi"},
    ),
    is_delete_operator_pod=True,
    dag=dag,
)
YAMLargo_workflow.yaml// Equivalent Argo WorkflowTemplate — lives in Git, no DAG-parse cycle
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: transform-events
  namespace: dataeng-pipelines
spec:
  entrypoint: transform
  arguments:
    parameters:
      - name: date
  templates:
    - name: transform
      inputs:
        parameters:
          - name: date
      container:
        image: registry/transform:2.1
        command: [python]
        args: ["/app/transform.py"]
        env:
          - name: DATE
            value: "{{inputs.parameters.date}}"
        resources:
          requests: { cpu: 500m, memory: 1Gi }
          limits:   { cpu: 2,    memory: 4Gi }

The YAML is more verbose. The runtime is dramatically simpler.

What we gained

  • DAG parse time eliminated — WorkflowTemplates are CRDs. There is no parser; kubectl apply is the parse step, and it costs nothing at runtime.
  • Scheduler RAM 8 GB → ~200 MB (Argo controller is lightweight; metadata lives in etcd as CRDs).
  • Failure isolation per workflow — one team's broken workflow no longer impacts other workflows. Resource quotas per namespace, no shared scheduler slot pool.
  • Native retries + exit-handlers — Argo has these as first-class fields on the template. We deleted ~600 LOC of custom Airflow retry/notification code.
  • Argo Events for triggers — S3 events, webhooks, cron, calendar, all wired the same way. Replaced 4 separate Airflow sensors with one event-driven CRD per source.

What we lost

  • The Airflow UI — Argo's UI is functional, not beloved. Our analysts had built mental models around the Airflow tree view; we spent two weeks retraining them on Argo. The pain was real and worth doing once.
  • The airflow.providers.* ecosystem — Airflow has an operator for every SaaS on earth. Argo expects you to build container images. Worth it long-term; painful in week one.
  • Backfills are different — Airflow's backfill semantics (run the DAG for date X) translated to "submit a Workflow with parameter date=X." We wrote a thin CLI that wraps this; it's 80 lines of bash and does the job.
  • Some teams' Cron habits — folks used to schedule via schedule_interval. Now they use Argo CronWorkflow. Same idea, different YAML — every team had to update their templates.

The migration protocol

We migrated in four phases over 6 weeks. The order matters; some of the harder pieces are easier to do early.

  • Week 1 — Argo in parallel, no migrations. Stood up the Argo controller in a new namespace. Wrote 3 trivial WorkflowTemplates (a hello-world, a Spark submit, a dbt run) to validate plumbing.
  • Weeks 2–3 — Migrate non-critical DAGs first. Picked DAGs that already used KubernetesPodOperator + had idempotent tasks. Wrote a converter script that read Airflow DAG Python and produced WorkflowTemplate YAML. Manual review on every output; trust the script to draft, not to ship.
  • Weeks 3–5 — Migrate the critical path. Order-of-magnitude harder. Required Argo Events configuration for upstream triggers (S3, Kafka). Ran both systems in parallel for 10 days; pages on Airflow ramped down as Argo took over each pipeline.
  • Week 6 — Decommission Airflow. Disabled all DAGs, kept the instance up for 30 days as fallback, then deleted. The 30-day window cost ~$3,000; it was the cheapest insurance we ever bought.
Ready to go deeper?

Build your Kubernetes foundation

The biggest barrier to this migration isn't the Argo YAML — it's having a real working knowledge of Kubernetes primitives (pods, deployments, CRDs, RBAC, events) when you start. Skip the foundation and you spend the migration debugging YAML instead of debugging your pipelines.

Our Kubernetes for Data Engineers module covers exactly this slice: the K8s primitives data engineers actually use, the operators you should know, Argo Workflows fundamentals, and the on-call playbook for when production starts paging at 2am.

Press Cmd+K to open