Airflow 2 to Airflow 3: what breaks and how to move
Airflow 2 reached end of life on 22 April 2026, so it gets no more fixes. The move to Airflow 3 is mostly done on Airflow 2: fix your Dags with ruff's AIR rules, install the standard provider, then migrate the database. The risky part is not the errors you see but the schedules that change without one.
Tecyfy · Published · 10 min read

Airflow 2 reached end of life on 22 April 2026, so it gets no more fixes, security fixes included. Most of the move to Airflow 3 happens while you are still on Airflow 2: you fix your Dags with ruff's AIR rules, switch to the new imports, then migrate the database in one step. The part that hurts teams is not the error messages. It is the 3 or 4 scheduling changes that throw no error at all.
Airflow 2Airflow 3
- 22 Apr 2025Airflow 3.0 ships. Tasks now reach the database only through the new API server.
- 25 Sep 2025Airflow 3.1. The old import paths start printing deprecation warnings.
- 22 Oct 2025Airflow 2 moves to limited maintenance.
- 7 Apr 2026Airflow 3.2.
- 22 Apr 2026Airflow 2 reaches end of life. 2.11 is its last line.
- 6 Jul 2026Airflow 3.3.
- 6 Nov 2026Airflow 3.4: task loops, Dags in other languages, psycopg3 by default, Python 3.11 or newer.
Release and support dates as Airflow publishes them on its announcements and supported versions pages. Source: Apache Airflow, Supported versions.
What changed in Airflow 3, underneath
In Airflow 3 your task code can no longer touch Airflow's own database. That one decision explains most of the upgrade.
In Airflow 2 every part of the system, the scheduler, the webserver and the workers running your tasks, held a connection to the metadata database. Task code ran in the same process as Airflow's internals, so a task could import a session and read or change any table. It was convenient and it was a mess: a bad task could block the database, and every worker needed database credentials.
Airflow 3 puts an API server in front of the database. It serves the UI, the REST API and a new Task Execution API, and workers talk to that instead. Tasks get what they need through the Task SDK, a stable set of imports under airflow.sdk. The Dag processor and the triggerer still run your Dag files with database access, but the code inside a task does not.
3 things follow from it:
- A task that queried Airflow's tables, to count failed runs or clean up old XComs, stops working. Use the Airflow Python client against the REST API with a token. Pointing a Postgres hook at the metadata database still works today, but Airflow warns that the schema is not a public API and that this route breaks from 3.2 onwards.
- The webserver is gone. You start
airflow api-serverinstead, and the Dag processor runs as its own process,airflow dag-processor, even on a laptop. - Imports move.
from airflow.decorators import dag, taskbecomesfrom airflow.sdk import dag, task,DatasetbecomesAsset, and the old paths print deprecation warnings from 3.1 on.
What breaks when you upgrade from Airflow 2
Pick what your own Dags and deployment use. Each item shows what breaks and when in the upgrade you fix it.
5 to fix
On Airflow 2, before you switch
BashOperator or PythonOperator imports
The core operators, sensors and triggers moved to a provider package.
Install apache-airflow-providers-standard on Airflow 2 and switch the imports there.
execution_date, prev_ds or next_ds
These context variables are gone. A Dag that still reads one fails.
Use logical_date or the data interval. ruff check dags/ --select AIR301 finds each one.
Cron schedules that read the data interval
A bare cron string now means CronTriggerTimetable, with no data interval.
Set create_cron_data_intervals = True before the upgrade. Changing it later skips one run.
During the upgrade
Nothing from your list.
Right after, before you trust the runs
Dags that rely on catchup by default
catchup_by_default is now False, so missed runs are not filled in.
Set catchup=True on the Dags that need it.
xcom_pull() without task_ids
It now reads only the current task's XComs and returns nothing from others.
Pass task_ids every time you read another task's value.
5 changes to make: 3 before the upgrade, 0 during the upgrade, 2 after the upgrade.
From Airflow's upgrade guide and the 3.4.0 release notes. The standard steps apply to every upgrade: be on 2.7 or later, back up the database, run airflow db clean, fix what ruff's AIR rules report, then airflow config update --fix and airflow db migrate.
Most of the list is fixed on Airflow 2, before anything is installed. The removals are the obvious ones: SubDAGs become TaskGroups or separate Dags linked by assets, SLAs become Deadline Alerts, and the CeleryKubernetes, LocalKubernetes and Sequential executors give way to several executors configured at once. The /api/v1 REST API is now /api/v2, and sign-in routes start with /auth, so an OAuth redirect such as /oauth-authorized/google becomes /auth/oauth-authorized/google.
How to upgrade from Airflow 2 to Airflow 3, step by step
Do it in this order. Each step is small, and only one of them touches production.
- Get to the latest Airflow 2. The upgrade guide needs 2.7 or later and recommends the newest 2.x release.
- Check your Python. Airflow 3.4 needs Python 3.11 or newer.
- Clean up. Back up the metadata database (shut Airflow down first if your database cannot take a hot backup), run
airflow db cleanto trim old rows so the schema migration runs faster, and make sureairflow dags reserializefinishes with no errors. - Fix the Dags on Airflow 2 with ruff, below, and install
apache-airflow-providers-standard, which now holdsBashOperator,PythonOperator,ExternalTaskSensorandFileSensor. It installs on Airflow 2, so the new imports can ship before the upgrade. - Take direct database access out of tasks.
- Upgrade. Install Airflow 3, then:
airflow config update # report settings that changed
airflow config update --fix # rewrite them for Airflow 3
airflow db migrate # the schema migration, the longest step
- Start the new processes.
airflow api-serverreplacesairflow webserver, andairflow dag-processorruns beside the scheduler. On the Helm chart, thewebserversettings move toapiServer. - Check sign-in. Simple Auth is now the default auth manager. To keep Flask-AppBuilder logins (OAuth, LDAP), install the FAB provider and set
auth_managerto itsFabAuthManager, and update any customwebserver_config.pyto the new security manager class.
Run the old and new deployments side by side on a copy of the database if you can. The next 2 sections are why.
How to find the breaking changes in your Dags
Ruff, the Python linter, has rules written by the Airflow project for this upgrade. Use ruff 0.13.1 or later. AIR301 and AIR302 flag what breaks; AIR311 and AIR312 flag what still works but should change.
ruff check dags/ --select AIR301 # list what breaks
ruff check dags/ --select AIR301 --show-fixes # show the fixes
ruff check dags/ --select AIR301 --fix # apply the safe ones
ruff check dags/ --select AIR301 --fix --unsafe-fixes # also rewrite import paths
The unsafe fixes change import paths and can change behaviour, so read the diff. Add the F401 rule afterwards to remove the imports left unused. Run this in CI on Airflow 2 and the Dags are ready before the platform team touches anything.
The changes that fail without an error
These are the ones to test with real runs, because nothing goes red.
Cron schedules. In Airflow 3 a bare cron string such as "0 6 * * *" means CronTriggerTimetable: the run happens at 06:00 and has no data interval to process. In Airflow 2 the same string meant "process the interval that just ended". A Dag that reads data_interval_start to pick yesterday's partition now gets something different. To keep the old meaning, set create_cron_data_intervals = True before you upgrade. Change it after Airflow 3 runs exist and one scheduled run is skipped. Our cron expression builder writes the cron Airflow reads and shows the next runs in your time zone, which helps when you compare old and new.
Catchup. catchup_by_default is now False. A Dag that relied on Airflow filling in missed intervals after a pause just carries on from today. Set catchup=True on the Dags that need it.
XCom reads. xcom_pull() with no task_ids used to search wider. In Airflow 3 it reads only the current task's values, so a downstream task gets None where it used to get data. Pass task_ids every time.
Manual runs. When someone triggers a run by hand with a date, don't assume the data interval matches it. Read logical_date when you need the date the person asked for.
Each of these is a pipeline that reruns cleanlyIdempotent Pipelines and Exactly-OnceA pipeline step is idempotent when running it twice gives the same result as running it once. That is what makes retries safe, and it is how systems that deliver a message more than once still count it exactly once. on Airflow 2 and quietly processes the wrong slice on Airflow 3. Compare row counts or partition dates between the old and new runs for a few days before you switch off Airflow 2.
Should you go straight to Airflow 3.4?
Yes, if you are starting now. There is no reason to land on 3.0 and upgrade again. Airflow 3.4.0 came out on 6 November 2026 and adds a few changes of its own to plan for:
- Python 3.11 or newer. 3.10 support is removed.
- psycopg3 for Postgres. A plain
postgresql://connection string is rewritten topostgresql+psycopg://. If something of yours needs psycopg2, install the matching provider extra. - One run per asset event. A Dag scheduled on a single asset, or on assets joined with OR, now gets one run for each event instead of one run for a batch of them. Set
batch_asset_events=Trueto keep batching. - More database connections. The API server opens a second async pool, so it can hold up to twice the connections your pool settings suggest. Check your database's connection limit.
- Worker heartbeats.
[workers] max_failed_heartbeatsis now the only retry control for heartbeats, and its default went from 3 to 30.
What task loops in Airflow 3.4 do
Task loops repeat a task group until a condition says stop, which Airflow could not express before without a sensor or a self-triggering Dag. A loop always runs once, stops when its until check returns true, and never runs more than max_iterations times. The shape of Airflow's own example:
from airflow.sdk import dag, task, task_group
@task
def improve(*, loop):
# loop.previous holds what the last pass returned
...
@task
def evaluate(estimate):
...
return {"estimate": estimate, "error": error}
@task_group
def refine():
return evaluate(improve())
def accurate_enough(*, loop):
return loop.result["error"] < 0.01
@dag
def refine_estimate():
refine.loop(max_iterations=10, until=accurate_enough)
refine_estimate()
until is a plain Python function, not a task. It gets the last pass's result and returns true or false. Loops cannot be nested or mapped as a whole, though the tasks inside one pass can be mapped. If you run a custom XCom backend that overrides serialize_value, it must also implement store_value, because each pass of a loop writes its own task instances.
3.4 also lets you write Dags in TypeScript, Go, Java or a YAML format. Airflow marks that framework and its language SDKs experimental, so keep production Dags in Python for now.
Where to start
Run ruff check dags/ --select AIR301 on your repository today, on Airflow 2. The number it prints is a fair measure of the work. Then list every Dag with a cron schedule that reads its data interval, and decide on create_cron_data_intervals before the upgrade, not after. If you are choosing an orchestrator rather than upgrading one, Airflow vs Dagster vs Prefect runs the same pipeline on all 3.
Questions
- Is Airflow 2 end of life?
- Yes. Airflow 2 went into limited maintenance on 22 October 2025 and reached end of life on 22 April 2026. Its last line is 2.11, and it gets no more fixes.
- What is the latest version of Apache Airflow?
- Airflow 3.4.0, released on 6 November 2026. It needs Python 3.11 or newer.
- Can I upgrade from Airflow 2 straight to Airflow 3?
- Yes, once you are on Airflow 2.7 or later. Airflow's upgrade guide recommends moving to the latest 2.x release first, fixing your Dags there, then running airflow db migrate on the new version.
- What replaced SubDAGs in Airflow 3?
- SubDAGs are removed. Use TaskGroups to group tasks inside one Dag, or split the work into separate Dags linked by assets.
- What replaced SLAs in Airflow 3?
- SLAs are removed and replaced by Deadline Alerts.
- What happened to execution_date in Airflow 3?
- execution_date, prev_ds, next_ds, yesterday_ds, tomorrow_ds and the other old context variables are removed, and a Dag that still uses one fails. Use logical_date or the data interval values. ruff check dags/ --select AIR301 lists every place they appear.
- Why can't my Airflow 3 task connect to the metadata database?
- In Airflow 3 tasks no longer talk to the metadata database directly. They go through the API server's Task Execution API. Code that needs Airflow's data should call the REST API with the Airflow Python client and a token.
- Does Airflow 3 still have a webserver?
- No. The webserver became the API server, started with airflow api-server, and the Dag processor now runs as its own process, started with airflow dag-processor, even in development.
- Why did my cron schedule change after upgrading to Airflow 3?
- In Airflow 3 a bare cron string uses CronTriggerTimetable, which has no data interval. To keep the Airflow 2 behaviour, set create_cron_data_intervals to True before you upgrade; changing it after Airflow 3 runs exist skips one scheduled run.
Sources