Why rehearse
Airflow 3 is a re-architecture: a separate API server and DAG processor, DAG bundles, a Task SDK and no direct metadata database access from tasks. The database migration is the largest and riskiest step. Recent release notes show how much can go wrong in the migration itself, for example:
- 3.3.1 fixed 2.x to 3.x upgrades that broke DAG triggering when a custom DAG bundle was configured.
- 3.3.1 deactivates legacy DAGs with a NULL
bundle_nameduring the upgrade. - 3.3.2 fixed
airflow db migratefailing under PyMySQL and XCom corruption in the bytea-to-JSONB migration.
Running the migration first on a copy of production data finds your problems without an outage.
1. Prepare the source
# clean old rows to shrink the migration (keep what your audit policy requires)
airflow db clean --clean-before-timestamp "2026-07-01" --skip-archive --yes
# make sure every DAG serialises cleanly
airflow dags reserialize
Fix any AirflowDagDuplicatedIdException or import errors first.
2. Fix DAG code with ruff
pip install "ruff>=0.13.1"
ruff check dags/ --select AIR301 --show-fixes # breaking changes
ruff check dags/ --select AIR302 # breaking changes in providers
ruff check dags/ --select AIR311,AIR312 # recommended updates
ruff check dags/ --select AIR301 --fix
Review --unsafe-fixes changes by hand; they rewrite import paths.
Common breakages the rules don't fully fix:
- Removed context keys (
execution_date,prev_ds,next_ds,yesterday_ds). xcom_pull()withouttask_idsnow only reads from the current task.catchup_by_defaultis nowFalse; bare cron schedules useCronTriggerTimetableunless you setcreate_cron_data_intervals = True.- SubDAGs, SLAs (replaced by Deadline Alerts) and some executors are gone.
- Tasks that open SQLAlchemy sessions to the metadata DB - see restricted metadata DB access.
3. Rehearse on a copy
# PostgreSQL example
pg_dump -Fc -h prod-db -U airflow airflow > airflow_prod.dump
createdb -h rehearsal-db -U admin airflow_rehearsal
pg_restore -h rehearsal-db -U admin -d airflow_rehearsal --no-owner airflow_prod.dump
Point a throwaway Airflow 3 deployment (same version you plan to ship) at the copy, with schedulers stopped so nothing runs against real systems:
airflow config update # report config changes
airflow config update --fix # apply them to a copy of airflow.cfg
time airflow db migrate
Record how long the migration takes; that's your outage window estimate. Then start the API server and DAG processor only, and check:
- All DAGs appear with the right bundle; no unexpected deactivations.
- Old DAG runs and task history are visible.
- A sample of XComs reads correctly.
- Connections and variables are intact.
Fix, rebuild the copy, and repeat until it's clean.
4. Cutover
- Announce the window; pause DAGs or stop the scheduler.
- Let running tasks finish (or note them for clearing).
- Take a final backup (
pg_dump, plus a storage snapshot if you have one). - Deploy Airflow 3; run
airflow db migrate. - Start the API server (
airflow api-server), DAG processor (airflow dag-processor), scheduler, triggerer and workers. - Unpause a few low-risk DAGs, then the rest.
5. Rollback plan
Rollback means restoring the backup and redeploying Airflow 2. Don't count on downgrading the schema across a major version. Write the restore steps down and time them during the rehearsal.
Checklist
-
db cleananddags reserializedone on the source - ruff AIR rules clean; manual fixes for context keys, XCom pulls, schedules
- Migration rehearsed on a restored copy with schedulers stopped
- Migration time measured; outage window agreed
- Post-migration checks passed on the copy
- Backup and restore-based rollback tested