Best Apache Airflow Hosting Platforms in 2026
Most Airflow hosting comparisons are a feature grid with checkmarks, and they answer a question nobody actually gets stuck on. Whether a platform "supports Airflow" is not the interesting part; all of them do. The things that decide whether your deployment is pleasant or miserable eighteen months from now are less glamorous: which Airflow version you are allowed to run, which executor you get, what happens when a DAG author pins a library that fights another DAG author's library, who owns the metadata database, what the whole thing costs while nothing is scheduled, and whose phone rings when the scheduler stops heartbeating at 3am.
I'm Ajay; I build PandaStack, a Firecracker microVM platform that appears in the field below and is emphatically not the top pick for most of this. So this roundup is organised around those decision triggers rather than as a leaderboard, and I keep it honest the only way that works for a vendor: concrete numbers appear only for our own system, every other platform is described qualitatively from its documented design, and you should verify pricing and limits against their current docs before you commit to anything. Managed-service constraints in particular change quietly and often.
The six triggers that actually decide this
Work down this list before you look at any vendor page. Whichever line makes you wince is the one that should pick your platform.
- Version and config control — can you run the Airflow version you want, when you want, and set the config keys you need? Managed services necessarily lag upstream and expose a subset of airflow.cfg. That is fine right up until a provider you depend on requires a newer Airflow than your platform offers.
- Executor choice — LocalExecutor, CeleryExecutor, or KubernetesExecutor is not a preference, it is an architecture. It determines where task code runs, how it scales, how isolated it is, and how much of the platform you are operating.
- Per-task isolation — DAG authors write arbitrary Python. On a shared worker that Python shares a Python environment, a filesystem, a kernel, and usually a set of credentials with every other DAG in the deployment.
- The metadata database — Airflow's Postgres is not a side-car. It is where scheduler state, task state, and XComs live, and an under-provisioned one presents as "Airflow is slow" rather than as a database problem.
- Cost at idle — a lot of Airflow runs four DAGs a day and bills for a scheduler, a webserver, a triggerer, and a warm worker pool for the other twenty-three hours. Ask what the floor is, not what the peak costs.
- Who carries the pager — the honest one. Somebody upgrades Airflow, drains the metadata DB, and gets paged when the scheduler loses its lock. Either you are paying a vendor for that or you are hiring for it.
The metadata database is the whole system
If you take one thing from this article, take this one. People treat the Airflow metadata database as configuration — a connection string you set once and never think about — and then spend a quarter wondering why the UI takes eight seconds to load and tasks sit in the queued state while the cluster is visibly idle.
Airflow is a database application that happens to run subprocesses. The scheduler's whole job is a loop over that database: find schedulable DAG runs, take row locks, decide what to queue, update state. Serialized DAGs live there. Task instance state lives there. XComs live there by default, which means every value your tasks pass to each other is a row someone will eventually have to clean up. Task logs are on object storage or disk, but the pointers and the run history are not. Lose the metadata DB and you have not lost a cache — you have lost the current state of every in-flight pipeline you own.
That has three consequences that show up in production and almost never in a getting-started guide.
- Connection pressure scales with parallelism, not with DAG count — every scheduler, every triggerer, every webserver process, and (on Celery) every worker process holds its own SQLAlchemy connection pool. Airflow's per-process pool defaults are small, but multiply them by replicas and worker concurrency and you reach a Postgres max_connections ceiling faster than anyone expects. On KubernetesExecutor with Airflow 2, every task pod also talks to the database, so max_active_tasks is a database sizing decision in disguise. The standard fix is a connection pooler in front — the official Helm chart ships PgBouncer as an option for precisely this reason.
- Scheduler latency is database latency — the scheduler takes row-level locks to hand out task slots, so a database that is CPU-starved, under-provisioned on memory, or sitting three network hops away turns into scheduling lag. "Tasks stay queued for two minutes" is nearly always this, and it is nearly always diagnosed as "Airflow is slow." Put the metadata DB close to the scheduler and give it a real instance, not the smallest tier that appeared in the dropdown.
- It needs backups and point-in-time recovery like any production database — because it is one. A metadata DB restored from last night's dump leaves you reconciling which tasks actually ran against which the database believes ran, by hand, under pressure. If your managed Airflow provider owns the database, find out their RPO. If you own it, own it properly.
The maintenance half is equally unglamorous. A few tables grow without bound and become the entire database if nobody looks: task_instance, xcom, log, and dag_run. Airflow ships a cleanup command for exactly this, and it should be on a schedule from day one rather than discovered during an incident.
-- Who is actually holding connections to the metadata database?
-- Run this when tasks are stuck in "queued" and the workers look idle.
SELECT application_name, state, count(*) AS conns
FROM pg_stat_activity
WHERE datname = 'airflow'
GROUP BY 1, 2
ORDER BY conns DESC;
-- The tables that quietly become the biggest thing in the database.
-- xcom is usually the surprise: someone returned a DataFrame from a task.
SELECT relname,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 10;
-- Retention is a cron job, not a heroic quarterly effort:
-- airflow db clean --clean-before-timestamp '2026-06-01' --yes
-- Check the flags for your Airflow version before running it anywhere real.Per-task isolation: the problem every Airflow shop lives with
The second structural issue is the one data platform teams complain about at conferences and rarely put in an architecture diagram. Airflow's programming model invites every team in the company to contribute DAGs, and a DAG is arbitrary Python. On a LocalExecutor or a Celery worker, that Python executes in one shared interpreter environment: one set of installed packages, one filesystem, one kernel, and one set of ambient credentials that were mounted for the worker rather than for the task.
Dependency hell follows immediately. The ML team wants one pandas, the finance DAG wants a different one, some vendor SDK pins an old protobuf, and now upgrading anything requires the agreement of four teams and a maintenance window. Blast radius follows too: a task that leaks memory takes down a worker running unrelated tasks; a task that writes to /tmp without cleaning up eventually fills the disk for everyone; a task that reads an environment variable it should not have read now has a credential it should not have had.
The standard answer is KubernetesExecutor, or KubernetesPodOperator on a Celery deployment. Each task gets its own pod from its own image, so dependencies stop colliding, resource requests and limits become per-task, and a task that dies takes only its own pod with it. This is a genuinely large improvement and it is the right default for most teams. Be precise about what it gives you, though: it gives you a container. Namespaces, cgroups, and a seccomp profile, sharing the host kernel with every other pod on that node. For internal ETL written by colleagues, that is a completely appropriate boundary, and I will say plainly that most Airflow deployments need nothing stronger.
It stops being appropriate at a specific and increasingly common point: when the code in the task is not yours. A customer-supplied transform, a notebook uploaded through your product's UI, an LLM-generated script, a partner's scoring function. At that point a shared kernel is a shared kernel, and "the task cannot escape the container" is a statement about the absence of known kernel bugs rather than about a boundary. A microVM per task changes the shape of that claim — the task gets its own guest kernel with a hardware virtualization boundary underneath — and a snapshot-restore create makes it cheap enough to do per task rather than per deployment. On PandaStack a create lands around 179ms p50 and 203ms p99, because there is no warm pool of idle VMs; every create restores a baked Firecracker snapshot on demand. A first cold boot of a brand-new template is about 3s, once.
The TaskFlow API makes the dispatch pattern tidy. This is a normal Airflow 2/3 DAG — the only unusual thing is that the task body creates a sandbox, runs the untrusted part inside it, and destroys it.
from __future__ import annotations
import json
from datetime import datetime, timedelta
from airflow.decorators import dag, task
from pandastack import Sandbox
from acme.registry import fetch_rows, load_customer_script, put_object
@dag(
schedule="0 3 * * *",
start_date=datetime(2026, 1, 1),
catchup=False,
default_args={"retries": 2, "retry_delay": timedelta(minutes=5)},
tags=["etl", "customer-code"],
)
def customer_transforms():
@task
def tenants() -> list[str]:
return ["acme", "globex", "initech"]
@task
def transform(tenant: str) -> dict:
"""Run one tenant's own transform script in its own microVM.
The script gets a fresh guest kernel, a fresh rootfs, and none of
this worker's credentials. It is destroyed when the task returns.
"""
sbx = Sandbox.create(
template="code-interpreter",
ttl_seconds=1800, # backstop: the VM cannot outlive the task
metadata={"tenant": tenant, "dag": "customer_transforms"},
)
try:
sbx.filesystem.write("/work/transform.py", load_customer_script(tenant))
sbx.filesystem.write("/work/in.json", json.dumps(fetch_rows(tenant)))
# Timeout INSIDE the task, shorter than the task's execution_timeout,
# so a runaway loop fails as a legible task error.
r = sbx.exec("python /work/transform.py", timeout_seconds=600)
if r.exit_code != 0:
raise RuntimeError(
f"{tenant} transform exited {r.exit_code}: {r.stderr[-2000:]}"
)
payload = sbx.filesystem.read("/work/out.json")
finally:
sbx.kill() # VM, disk, and network namespace go away together
# Return a POINTER, not the payload. XComs land in the metadata
# database, and that database is load-bearing for the scheduler.
key = put_object(f"transforms/{tenant}/out.json", payload)
return {"tenant": tenant, "s3_key": key, "bytes": len(payload)}
transform.expand(tenant=tenants())
customer_transforms()Two details generalise well beyond us. Put a hard timeout inside the task that is shorter than the task's own execution_timeout, so a runaway loop surfaces as a task failure rather than a hung worker slot. And decide idempotency deliberately, because retries: 2 means the customer's code runs again — possibly somewhere else — and for anything with side effects that is a correctness question rather than a resilience feature.
The field
Each option gets what it is, who it suits, and where it hurts. Nothing below invents a price, an SLA, or a benchmark for anyone but us — verify current versions, quotas, config allowlists, and pricing against each vendor's own documentation, because this category ships changes faster than roundups get updated.
1. Astronomer (Astro)
The commercial Airflow company, staffed with a large share of the project's committers, selling a managed Airflow platform plus the surrounding tooling: a local development CLI that mirrors the deployed runtime, a deploy path from a repo, Airflow-aware observability and lineage, and support from people who can read the scheduler's source. If Airflow is strategic to your business — dozens of DAG authors, orchestration as a company-wide function — this is the default answer and the one you should have a reason to reject.
- Who it's for — organisations where Airflow is core infrastructure rather than a side project, and where a support contract with people who write Airflow is worth real money.
- Where it hurts — it is a commercial platform with commercial pricing, and you are on their runtime and their deployment model. Verify current pricing, version support windows, and deployment options against Astronomer's own docs; do not size a budget from a blog post.
2. Amazon MWAA
Managed Workflows for Apache Airflow runs Airflow inside your AWS account, wired into the parts of AWS you already use: VPC placement, IAM execution roles instead of long-lived credentials in Airflow connections, CloudWatch for logs and metrics, S3 as the delivery mechanism for DAGs, requirements, and plugins. Its real superpower is procurement — it is a line on an AWS bill, which for a lot of enterprises is the difference between shipping this quarter and a six-month vendor review.
- Who it's for — teams whose data already lives in AWS, whose security review is easier when nothing leaves the account, and who want orchestration without a platform team.
- Where it hurts — control. Available Airflow versions trail upstream, config is limited to an allowlist of overrides rather than a free hand on airflow.cfg, dependency installation goes through a requirements file with its own constraints, and you do not get shell access to debug the thing. Check the current supported versions, environment classes, and configuration allowlist in the AWS docs before assuming a specific provider or Airflow feature is available.
3. Google Cloud Composer
The GCP equivalent, and the trade has the same shape: managed Airflow that sits natively next to BigQuery, GCS, and Dataproc, with IAM and Cloud Logging already wired up. Composer runs on GKE under the hood, which surfaces in both directions — it means autoscaling and Kubernetes-native task execution are natural, and it means there is a cluster's worth of concepts underneath a product that is trying to hide them from you.
- Who it's for — GCP-native data platforms, especially anything BigQuery-centric where the operators and the IAM story are already the path of least resistance.
- Where it hurts — same category of constraint as MWAA on versions and configuration, plus an environment that has a real cost floor whether or not your DAGs are doing anything. Verify the current Composer generation, its version support policy, and its pricing components against Google's docs.
4. Azure: Managed Airflow in Data Factory
Azure's answer arrived as a Managed Airflow capability inside Azure Data Factory, giving Azure shops a hosted Airflow environment alongside ADF pipelines rather than as a standalone product. Listed here mostly so the roundup is complete: if you are an Azure estate and you need Airflow specifically — because your DAGs already exist, or your team knows Airflow and does not want to learn ADF's model — it exists and it is the low-friction choice. Microsoft has been reshuffling this space between Data Factory and Fabric, so check what the current, generally available product is called and what Airflow versions it supports before you plan around it.
- Who it's for — Azure-committed teams with existing Airflow DAGs, or ones who want Airflow's ecosystem of providers rather than ADF's activity catalogue.
- Where it hurts — it is the least mature of the three hyperscaler offerings and the most subject to productization churn. Verify current status, limits, and pricing in Microsoft's own docs, and weigh whether the honest answer for a greenfield Azure pipeline is simply to use the native tooling.
5. Self-hosting on Kubernetes
The official Helm chart plus KubernetesExecutor is the maximum-control option and, for a team that already runs Kubernetes well, a genuinely good one. You choose the Airflow version and upgrade on your schedule, you set any config key you like, DAGs arrive by git-sync or a baked image, each task gets its own pod with its own image and its own resource limits, and the chart ships the pieces you will need anyway — PgBouncer for the metadata connections, a triggerer for deferrable operators, log persistence options.
The cost is that you now operate Airflow, which is a job. Scheduler HA, Airflow upgrades that include database migrations, metadata DB retention, node capacity for bursty task pods, log shipping, and secrets. None of it is exotic; all of it is continuous.
# values.yaml for the official apache-airflow/airflow chart.
# The three settings that matter most are all about the database and isolation.
executor: KubernetesExecutor
postgresql:
enabled: false # no in-cluster StatefulSet pretending to be a database
data:
# Point at a real managed Postgres with backups and PITR. This database is
# the source of truth for every in-flight task you own.
metadataSecretName: airflow-metadata-db
pgbouncer:
enabled: true # schedulers, triggerers, and task pods all open conns
maxClientConn: 500
metadataPoolSize: 12
scheduler:
replicas: 2 # HA scheduler; both take locks against the same DB
dags:
gitSync:
enabled: true
repo: git@github.com:acme/airflow-dags.git
branch: main
subPath: dags
config:
core:
parallelism: "64" # a database sizing decision wearing a scheduler hat
max_active_tasks_per_dag: "16"
# Verify key names against the chart version you are installing — this chart
# renames things between releases, and Airflow 3 renamed components too.6. Self-hosting on a PaaS or plain VMs (including PandaStack)
The under-rated option. A scheduler and a webserver on one machine with LocalExecutor, pointed at a managed Postgres, will comfortably run the DAG load of most companies that think they need a cluster. It is one process tree, one log file, and one upgrade to perform. You give up per-task pod isolation and horizontal worker scaling, and you get back an architecture a single person can hold in their head.
#!/usr/bin/env bash
# Airflow on one VM: constraints-pinned install, managed Postgres, systemd.
set -euo pipefail
AIRFLOW_VERSION=2.10.5
PY=3.12
# Airflow MUST be installed with its constraints file. Skipping this is the
# single most common way to end up with an Airflow that imports but does not run.
pip install "apache-airflow==${AIRFLOW_VERSION}" \
--constraint "https://raw.githubusercontent.com/apache/airflow/constraints-${AIRFLOW_VERSION}/constraints-${PY}.txt"
# The metadata DB is a separate, backed-up, managed Postgres. Not localhost.
export AIRFLOW__DATABASE__SQL_ALCHEMY_CONN="postgresql+psycopg2://airflow:${PGPASS}@db.internal:5432/airflow?sslmode=require"
export AIRFLOW__CORE__EXECUTOR=LocalExecutor
export AIRFLOW__CORE__PARALLELISM=32
export AIRFLOW__CORE__LOAD_EXAMPLES=False
airflow db migrate
airflow users create --role Admin --username admin --email ops@acme.dev \
--firstname A --lastname K --password "${ADMIN_PASS}"
# Two long-lived processes, restarted by systemd, and that is the whole install.
# On Airflow 3 the webserver is the api-server; check component names for your
# version before copying these unit names.
systemctl enable --now airflow-scheduler airflow-webserver
# Retention, from day one, not from the first incident:
# 0 4 * * 0 airflow db clean --clean-before-timestamp "$(date -d '-90 days' +%F)" --yesWhere we fit: PandaStack gives you a Firecracker microVM with a real Linux userspace to run that scheduler and webserver in, and a managed Postgres for the metadata database (create runs 30–90s). That is the ordinary, unglamorous half. The differentiated half is the one from the isolation section — tasks that dispatch into their own microVM, created per task at roughly 179ms p50, or forked from a warm parent in 400–750ms same-host and 1.2–3.5s cross-host when you want the environment pre-warmed. Billing is per-second at $0.054 per active vCPU-hour and $0.0162 per GiB-hour, so a sandbox that exists for ninety seconds a night costs approximately nothing, which is the shape most Airflow task workloads actually have.
- Who it's for — teams who want Airflow without a Kubernetes practice, and teams whose tasks execute customer-supplied or otherwise untrusted code and need a per-task boundary stronger than a shared kernel.
- Where it hurts — there is no managed Airflow control plane here. You install Airflow, you upgrade Airflow, you run the database migration, and you carry the pager for the scheduler. If what you wanted was somebody else's problem, buy Astro or MWAA and skip this row.
The comparison, in one list
Qualitative and subject to change for everything except our own numbers. Use it to build a shortlist, then verify the specifics against each vendor's current documentation.
- Astronomer (Astro) — Operated by: them. Version/config control: strong for a managed product, on their runtime and support windows. Per-task isolation: Kubernetes-based task execution. Metadata DB: theirs. Idle cost: a real platform floor — check current pricing. Lock-in: moderate; the DAGs are portable Airflow, the tooling around them is not.
- Amazon MWAA — Operated by: AWS, in your account. Version/config control: constrained — trailing versions, an allowlist of config overrides, no shell. Per-task isolation: shared workers by default; KubernetesPodOperator against EKS if you want more. Metadata DB: AWS-managed and opaque to you. Idle cost: environment-based, bills whether or not DAGs run. Lock-in: IAM, S3 delivery, CloudWatch — moderate and AWS-flavoured.
- Google Cloud Composer — Operated by: Google, on GKE. Version/config control: same category of constraint as MWAA. Per-task isolation: Kubernetes-native task execution is natural here. Metadata DB: Google-managed. Idle cost: environment-based floor. Lock-in: moderate; deep BigQuery/GCS gravity.
- Azure Managed Airflow (Data Factory) — Operated by: Microsoft. Version/config control: the most constrained of the three, and the most subject to product churn. Per-task isolation: managed workers. Metadata DB: Microsoft-managed. Idle cost: environment-based. Lock-in: moderate, plus the risk of the product being reshaped around you.
- Self-hosted on Kubernetes — Operated by: you. Version/config control: total. Per-task isolation: KubernetesExecutor gives a pod per task — a container sharing the node kernel. Metadata DB: yours, and you must size, pool, back up, and prune it. Idle cost: cluster plus scheduler plus webserver plus triggerer, always. Lock-in: none beyond Kubernetes itself.
- Self-hosted on a PaaS or VMs (incl. PandaStack) — Operated by: you, with much less surface. Version/config control: total. Per-task isolation: LocalExecutor shares everything by default; on PandaStack a task can dispatch into its own microVM with its own guest kernel. Metadata DB: yours — managed Postgres, create 30–90s on ours. Idle cost: on PandaStack, per-second billing at $0.054/vCPU-hour and $0.0162/GiB-hour means idle sandboxes approach zero; the scheduler VM itself still runs. Lock-in: low; it is stock Airflow on a Linux box.
The uncomfortable question: do you need Airflow?
A meaningful share of production Airflow deployments are a cron job wearing a DAG costume. Three tasks in a straight line, once a day, no branching, no backfills, no dependency between runs — and around it, a scheduler, a webserver, a triggerer, a worker pool, a Postgres instance, and a quarterly upgrade project. Airflow is excellent at what it was built for. What it was built for is complex dependency graphs across heterogeneous systems with scheduled backfills and a UI that data analysts can use to see why yesterday's run is missing. If your pipeline is not that, you are paying a substantial operational tax for a UI.
The honest alternatives, briefly and fairly, because a roundup that pretends every reader needs the thing it is reviewing is an advertisement.
- Plain cron or a hosted scheduler — if it is a sequence of scripts on a schedule with no fan-out and no backfill, a scheduled job plus decent alerting is the correct answer and it will still be correct in three years. This is not a downgrade; it is right-sizing.
- Dagster — asset-oriented rather than task-oriented: you declare the tables and models you want to exist and it works out the graph, with typed inputs/outputs and local testing as first-class concerns. Data teams migrating away from Airflow for developer-experience reasons usually land here.
- Prefect — closest in spirit to "Airflow but Pythonic," with dynamic flows defined in ordinary Python control flow rather than a DAG structure resolved at parse time. Attractive when your pipeline shape is genuinely dynamic and Airflow's parse-time model keeps fighting you.
- Temporal — a different category that overlaps confusingly: durable execution for long-running, retry-heavy, stateful workflows. If your "pipeline" is really a business process with human steps, timeouts, and compensation logic, you want a workflow engine, not a data orchestrator.
- Your warehouse's own scheduler — dbt Cloud, BigQuery scheduled queries, Snowflake tasks. If the entire pipeline is SQL inside one warehouse, an external orchestrator is a second system that mostly exists to say "go".
The tell is simple. If nobody has ever opened the Airflow UI to investigate a failure, and no DAG has more than one branch, you bought an orchestrator to run a shell script. Migrating away is work, so this matters most for greenfield decisions — but it is worth ten honest minutes before you sign a platform contract.
How to choose
- Your graph is three tasks in a line, once a day — do not deploy Airflow. Use a scheduled job with real alerting and revisit when you have branching or backfills.
- Airflow is strategic, you have many DAG authors, and you have budget — Astronomer. Deepest Airflow-specific tooling and support, and the fastest path to Airflow being somebody's product rather than your side project.
- Your data and your security review both live in one cloud — MWAA, Composer, or Azure's Managed Airflow, in that order of maturity. Accept the version and config constraints deliberately, and confirm the current allowlist before you depend on a specific feature.
- You already operate Kubernetes competently and want full control — the official Helm chart with KubernetesExecutor. Budget for the metadata DB, PgBouncer, retention, and upgrade work as ongoing, not one-off.
- You want Airflow without a cluster — one VM, LocalExecutor, managed Postgres. Boring, legible, and sufficient far further up the scale than people expect.
- Your tasks run code you did not write — the isolation boundary is now the top requirement rather than a footnote. That means a microVM per task, on PandaStack or on Kubernetes with gVisor or Kata if you would rather own it.
Whatever you shortlist, prove it with a spike rather than a spreadsheet. Deploy the candidate, point it at a metadata database deliberately sized one tier too small, and run a DAG that fans out to fifty mapped tasks. You will learn more about that platform in an afternoon than any comparison table can tell you — including this one — because the failure you are shopping to avoid is not a missing feature. It is the scheduler quietly falling behind while every dashboard says green.
Frequently asked questions
Should I use managed Airflow or self-host it?
Use managed Airflow if orchestration is not something your team wants to operate, and the version and configuration constraints do not block you. That is the real test: managed services trail upstream Airflow and expose only a subset of configuration, so check the current supported versions and config allowlist against a provider or feature you actually depend on before committing. Self-host when you need a specific Airflow version now, need config keys the managed service will not expose, need per-task isolation stronger than the platform provides, or when you have modelled the cost and it favours you. Wanting to avoid a vendor is not the same as having modelled it, and the operational load — upgrades with database migrations, metadata DB retention, scheduler HA — is continuous rather than one-off.
How should I size the Airflow metadata database?
Size it for connections and latency first, storage second. Every scheduler, triggerer, webserver process and — depending on your executor and Airflow version — every worker or task pod holds its own connection pool, so your Postgres max_connections ceiling is a function of parallelism, not of how many DAGs you have. Put a pooler like PgBouncer in front, which the official Helm chart ships as an option for exactly this reason. Then keep the database close to the scheduler, because the scheduler takes row locks in a tight loop and database latency presents to users as tasks sitting in the queued state. Finally, treat it as a production database: backups and point-in-time recovery, plus a scheduled cleanup of task_instance, xcom, log, and dag_run before they become the whole database.
KubernetesExecutor or CeleryExecutor — which should I pick?
KubernetesExecutor if you already run Kubernetes and your tasks have conflicting dependencies or very different resource profiles, because each task gets its own pod from its own image with its own limits, which solves dependency hell and shrinks blast radius. CeleryExecutor if you have many short tasks and want warm workers without paying pod startup on every one, accepting that those workers share a Python environment and a kernel. LocalExecutor is genuinely fine for a large fraction of deployments and is the option people skip out of embarrassment rather than analysis. Note that per-pod isolation is container isolation: a shared node kernel, which is appropriate for internal ETL and not sufficient when the task body is code you did not write.
Can I run each Airflow task in its own VM?
Yes, and it is the right pattern when tasks execute untrusted or customer-supplied code. The task body creates a microVM, writes in the code and inputs, executes with a timeout shorter than the task's own execution_timeout, reads the output, and destroys the VM — so each task gets its own guest kernel and filesystem and none of the worker's ambient credentials. The objection is usually latency, which is a snapshot question: cold-booting a VM per task is a multi-second tax, while restoring a pre-baked snapshot is not. On PandaStack a create is roughly 179ms at p50 and 203ms at p99, with forking from a warm parent at 400–750ms on the same host. For ordinary internal pipelines, a container per task is fine and simpler — reach for a VM boundary when the code is not yours.
Is Airflow overkill for my pipeline?
Possibly. Airflow earns its operational cost when you have complex dependency graphs across heterogeneous systems, scheduled backfills, and non-engineers who need a UI to see why yesterday's run is missing. If your pipeline is three scripts in a straight line once a day, you are running a scheduler, a webserver, a triggerer, a worker pool, and a Postgres instance to do what cron does, plus a quarterly upgrade project. Consider a plain scheduled job with real alerting, Dagster if you think in terms of the tables and models that should exist, Prefect if your flows are genuinely dynamic, Temporal if what you have is really a long-running business process, or your warehouse's own scheduler if the whole pipeline is SQL in one place.
Keep reading
- Best Temporal hosting platforms in 2026 — The durable-execution neighbour — worth reading if your DAG is really a business process.
- Best cron job platforms in 2026 — For the honest majority of pipelines that never needed a DAG in the first place.
- Best managed Postgres providers in 2026 — The metadata database decision, which is the one that actually decides whether Airflow feels fast.
- Postgres connection pooling explained — Why parallelism, not DAG count, is what exhausts your metadata DB connections.
- Per-tenant isolation for ETL pipelines — The deeper version of the customer-supplied-transform problem.
49ms p50 cold start. Fork, snapshot, and scale to zero.