MWAA vs Composer vs Astronomer vs self-hosted, what Airflow 3 changed, and the deprecations that still bite when you follow an old tutorial.
⏱ 12 min readTopics chapter readerLevel · Medium
01 · Start here
Start Here
⏱ 4 min · Topic 1 of 10
Two decisions close this track. Should your team run Airflow itself, or pay a company to run it? And how do you move from Airflow 2 to Airflow 3?
You follow the same DAG as before: shop_orders_daily. You also meet a made-up platform with 40 DAGs, the same company as in Executors Compared: Local, Celery, Kubernetes. Every number about it is made up.
Airflow 3 came out in April 2025. Services and versions change fast, so check the provider docs and the release notes for the latest facts. You answer four questions:
What does a managed service run for you, and what stays yours?
How do you count the real cost?
How do you plan the move to Airflow 3?
Which changes break loudly, and which change data without an error?
02 · Who runs it
What Managed Airflow Is
⏱ 8 min · Topic 2 of 10
Managed Airflow means a company runs the Airflow parts for you. You still write the DAGs.
A bucket is a named place for files in cloud storage. Three managed services are common. Look at how each one receives your DAG files:
Self-hosted and three managed services
Option
Who runs Airflow
How your files arrive
Watch for
Self-hosted
Your team
Your own tools
Everything is yours to run
Amazon MWAA
AWS
DAG files and requirements.txt go to an S3 bucket
AWS decides which versions you can choose
Google Cloud Composer, Google’s managed Airflow
Google, on its Kubernetes service
DAG files go to a Cloud Storage bucket
Airflow 3 runs only on Composer 3
Astronomer (Astro)
Astronomer
You build an image and deploy it with the Astro command-line tool (CLI)
The deploy tools belong to Astronomer
The DAG file is the same in all four cases. Only the way you ship it changes. Look at one DAG on three services:
Ship shop_orders_daily three waysworked example
Shell
# Amazon MWAA: copy the file to the DAGs folder in S3
aws s3 cp shop_orders_daily.py s3://shop-demo-airflow/dags/
# Google Cloud Composer: import the file to the environment bucket
gcloud composer environments storage dags import \
--environment shop-demo --location europe-west1 \
--source shop_orders_daily.py
# Astronomer: build the project image and deploy it
astro deploy
Result · 3 rows
Service
What you ship
DAG code changes?
Amazon MWAA
One file in S3
No
Google Cloud Composer
One file in a bucket
No
Astronomer
A project image
No
Your DAGs stay portable. Your deploy scripts do not.
The bucket, environment and location names are made up.
Now the split of work. Look at who owns each job under a managed service:
Job
Under a managed service
Metadata database
The provider runs it
Scheduler and API server
The provider runs them
Worker scaling
The provider scales, inside limits you set
Airflow upgrade
You choose when
DAG code and tests
You
Python packages
You
Connections and secrets
You
Alerts and fixing failed pipelines
You
The bill
You, as a service fee
Managed services also have limits. Check each one in the provider docs, because they change often:
Versions: a service often offers a new Airflow release months after Airflow ships it.
Plugins and settings: some settings and custom code may be limited.
Network: reaching a private database needs network set-up on your side.
Watch out
Managed removes the on-call for Airflow parts. It does not remove the on-call for your pipelines. A DAG that loads wrong rows is still yours.
03 · The honest sum
Count the Real Cost
⏱ 6 min · Topic 3 of 10
A price list shows servers for self-hosting and a fee for managed. It leaves out the hours your team spends.
Think of the made-up platform with 40 DAGs. A platform engineer keeps Airflow running for the other teams. The hours below are made up, per year.
Engineer hours per year (made up)
Work
Self-hosted
Managed
Version upgrades
120 hours
24 hours
Metadata database care
60 hours
0 hours
Worker sizing and scaling
40 hours
8 hours
Airflow incidents and on-call
80 hours
10 hours
Total
300 hours
42 hours
Self-hosted: 120 + 60 + 40 + 80 = 300. Managed: 24 + 0 + 8 + 10 = 42. The gap is 258 hours.
Managed still needs 24 hours for upgrades. You test your 40 DAGs on each new version, even when the provider does the upgrade.
Now compare in words. Self-hosting costs servers plus 300 hours. Managed costs a service fee plus 42 hours.
Ask what 258 hours of your team are worth.
Measure your real hours for one month before you decide. A guess made on day one can be far off.
Remember
Add the hours. A server bill and a service bill are only half of the comparison.
04 · The migration
From Airflow 2 to Airflow 3
⏱ 9 min · Topic 4 of 10
Airflow 3 is the biggest change since Airflow 2.0. A migration is the move of a running system to a new version.
You met the Airflow 3 changes in Airflow Architecture: Scheduler, Executor, Workers, Metadata DB. The API server replaces the web server, and the DAG processor runs alone. A task no longer reads the metadata database.
This section is the plan to get there.
Follow these eight steps in order:
Inventory: list every DAG, plugin, custom operator, setting and script that calls the API.
Bridge: you can upgrade to Airflow 3 only from Airflow 2.7 or later. The upgrade guide advises the latest 2.x release first.
Fix on 2.x: remove deprecation warnings (notes that a feature will go away) and run the ruff checks (next section).
Back up the metadata database. Run airflow db clean first, so the database is smaller.
Stage: upgrade a copy of the platform and run real DAGs on it.
Run both: keep Airflow 2 running and move DAGs to Airflow 3 in waves.
Cut over: when the last wave works, switch off Airflow 2 for good.
Rollback plan: keep the old environment and the backup for a set time.
On Amazon MWAA and Google Cloud Composer, you cannot upgrade Airflow 2 to Airflow 3 in place. You create a new Airflow 3 environment and move DAGs to it.
Airflow 3 also records DAG versions. The UI shows which version of a DAG each run used.
Look at the waves for the made-up platform. It has 40 DAGs, so you move them in three groups:
Three waves (made up)
Wave
DAGs moved
Moved so far
Check before the next wave
1: pilot
4, with shop_orders_daily
4
Rows match Airflow 2 for a past day
2: one team at a time
12
16
Alerts fire and nothing runs twice
3: the rest
24
40
All DAG tests pass on Airflow 3
Check the sums: 4 + 12 + 24 = 40.
Each DAG lives in one place at a time. Pause it on Airflow 2 before you turn it on in Airflow 3.
The rollback plan matters because you should treat the database change as one way. You go back by switching to the old environment, not by undoing the database change. Check the release notes for what your version supports.
Watch out
A DAG that is on in both places runs twice. Both runs write the same table, so you get double rows.
Interview question
What makes the Airflow 2 to 3 upgrade risky? A strong answer names the silent changes, not the loud ones. It also gives a staged plan with a rollback.
05 · Tools
Let Tools Find Changes
⏱ 7 min · Topic 5 of 10
Do not search your DAGs by hand. Ruff is a Python checker, and it has rules for Airflow. Airflow also checks your settings.
Look at the commands, then at an Airflow 2 file. Use ruff 0.13.1 or newer, as the upgrade guide says:
The upgrade checksworked example
Shell
# Find code that Airflow 3 changes
ruff check dags/ --select AIR3
# Report settings that Airflow 3 changes
airflow config lint
# Show the setting changes, or apply them
airflow config update
airflow config update --fix
Names are from the Airflow 3 upgrade guide. Check it for your version.
Now the file ruff reads. It is shop_orders_daily written for Airflow 2:
shop_orders_daily on Airflow 2worked example
Python
from datetime import datetime
from airflow.decorators import dag, task
from airflow.datasets import Dataset
orders_table = Dataset("postgres://shop/orders")
@dag(schedule_interval="0 0 * * *", start_date=datetime(2026, 1, 1))
def shop_orders_daily():
@task(outlets=[orders_table])
def load_orders(**context):
day = context["execution_date"].date()
print(f"loading {day}")
load_orders()
shop_orders_daily()
Result · 4 rows
Line
Finding
Kind
2
airflow.decorators moved to airflow.sdk
Suggested (AIR311)
3
Dataset is now Asset, in airflow.sdk
Suggested (AIR311)
7
schedule_interval is now schedule
Must fix (AIR301)
11
execution_date is now logical_date
Must fix (AIR301)
Rule numbers can differ between ruff versions. Ruff cannot see that line 7 sets no catchup.
The address (URI) is made up.
Line 7 has a second problem that no tool reports. The next sections explain it. Look at the file after the fixes:
All Airflow imports now come from airflow.sdk. The catchup value is now written down on purpose.
A timetable is the rule that turns a schedule into runs. Read the changes, and do not trust them blindly.
In this file, logical_date does not mean the same day under the new default timetable. Two Silent Changes shows why.
06 · Loud changes
What Airflow 3 Removed
⏱ 5 min · Topic 6 of 10
A removed feature fails loudly. Your DAG file stops importing, and you see the error at once.
Look at what to search for. Each row has a replacement, and most replacements come from earlier chapters:
Removed or changed
Use instead
SubDAGs
A TaskGroup
SLA misses (sla, sla_miss_callback)
Deadline alerts, added in Airflow 3.1
SequentialExecutor
LocalExecutor for tests
Hybrid executors, such as CeleryKubernetesExecutor
A list of executors
The context key execution_date
logical_date
schedule_interval and timetable arguments
schedule
Direct metadata database access in a task
The REST API, or the Python client
The webserver command
airflow api-server
DAG processor inside the scheduler
airflow dag-processor as its own process
Check the release notes for the full list. Airflow 3.1 and later may add or bring back features.
If a task counts earlier runs through the database, rewrite it to call the API. Also check scripts that call the old REST API path. Airflow 3 uses a new one.
Start-up scripts and health checks also change. A script that starts the webserver must now start the API server and the DAG processor.
Remember
Imports that pass do not mean you are done. The next section covers changes that raise no error.
07 · Quiet changes
Two Silent Changes
⏱ 9 min · Topic 7 of 10
Some changes run without any error. The DAG imports and the task turns green, but the data is different.
A tick is a moment when the schedule fires.
The first change is the timetable. A plain cron string used to make one run for each full day. In Airflow 3, it makes a run at the tick, and the start equals the end.
Use the six shop orders from Scheduling, Data Intervals & the Logical Date Trap. Look at the paid revenue query for the tick 2026-01-11 00:00:
The same query on Airflow 2 and Airflow 3worked example
On Airflow 2 the window is 2026-01-10, and only order 102 is paid that day: 25.00. On Airflow 3 the window is empty, so the revenue is 0.00.
The double braces are Jinja: Airflow fills in the dates before the query runs.
The Airflow 3 run is green, but it built the wrong day. Its 0.00 looks right only because order 103 is refunded. Order 102 on 2026-01-10 is never counted.
You have two fixes. Set create_cron_data_intervals = True in the scheduler settings to keep the old way.
Or pass the timetable in the DAG, as in Scheduling, Data Intervals & the Logical Date Trap. Look at the schedule line:
The logical_date is 2026-01-10 for the 2026-01-11 tick again, as on Airflow 2.
Only the import and the schedule value change. The rest of the DAG stays the same. Newer Airflow 3 docs import it from airflow.sdk.
The second change is catchup. Catchup decides whether Airflow builds the days that are already over. Its default was True in Airflow 2 and is False in Airflow 3.
Take a new DAG with start_date 2026-01-01, deployed on 2026-01-11, with no catchup setting. Airflow 2 builds the 10 old days, 2026-01-01 to 2026-01-10. Airflow 3 does not build them.
So a DAG that built its own history may now start with none. Set catchup on every DAG, so no default decides for you.
Watch out
After the upgrade, run one DAG for a past day and count the rows. If the count is 0, check the timetable first.
08 · Afterwards
Stay Current
⏱ 5 min · Topic 8 of 10
Most Airflow articles describe Airflow 2, and some describe Airflow 1. You can often date an article by its words.
Use the table in What Airflow 3 Removed as a check list. If an article uses a word from its left column, it was written for Airflow 2 or older.
An article about SubDAGs was written for an old Airflow. Treat the rest of its advice with care.
Upgrade in small steps. If you wait for five releases, the next upgrade becomes a project, and a security notice often forces it.
Run the ruff Airflow rules in your continuous integration (CI) checks all the time, not only on upgrade day. They show new deprecations while you have time to fix them.
09 · Recap
Quick Recap
⏱ 5 min · Topic 9 of 10
Managed Airflow takes away the Airflow parts. Your DAGs, packages, secrets, alerts and bill stay yours.
Look at the answers you should know:
Question
Short answer
What do you count in the cost?
Servers or fee, plus engineer hours.
What is the first migration step?
An inventory, then the latest Airflow 2.x release.
How do you move 40 DAGs?
In waves, with each DAG on only one Airflow at a time.
What do ruff and airflow config find?
Removed code and changed settings.
Which changes raise no error?
The cron timetable and the catchup default.
What is the rollback plan?
Keep the old environment and the backup.
Try this alone. After an upgrade, a green DAG built 0.00 revenue for a day that had paid orders. What do you check first?
The answer is the timetable. Under the new default, the data interval start equals its end, so the window is empty.
10 · You have finished the knowledge base
You have finished the knowledge base
You now know why orchestration exists, how Airflow is built, and how to write DAGs that survive retries and backfills.
⏱ 3 min · Topic 10 of 10
Next chapter
Apache Airflow
You now know why orchestration exists, how Airflow is built, and how to write DAGs that survive retries and backfills.
You also know how to run them at scale and how to move to Airflow 3. Go back to any chapter to learn more, then build something.