Running a backfill without flooding the source system, retry and timeout settings that behave, and the Deadline Alerts that replaced SLAs in Airflow 3.
⏱ 5 min readTopics chapter readerLevel · Medium
01 · Start here
Start here
⏱ 4 min · Topic 1 of 8
Things go wrong in every pipeline. This chapter answers four questions, each with its own setting:
Question
Setting
A task failed. Should it try again?
retries
How long may it run?
execution_timeout and dagrun_timeout
Who is told, and when?
Callbacks and deadline alerts
How do I run past days, or repair them?
Backfill, clear and mark success
Most repairs here run tasks again. So they only work if a task is safe to run twice, which is the topic of chapter 13.
02 · Retries
Retries, and how long they take
⏱ 6 min · Topic 2 of 8
Many failures fix themselves: a short network problem, a busy API, a database that is still starting. Retries try the task again after a wait:
Retries for the whole DAG, and one exceptionworked example
Python
@dag(
...,
default_args={"retries": 2, "retry_delay": timedelta(minutes=5)},
)
def shop_orders_daily():
@task # gets 2 retries from default_args
def load_orders(): ...
@task(retries=0) # never retry: it would email twice
def email_report(): ...
default_args gives every task the same start values. A setting on one task overrides them.
Retries delay the alert. on_failure_callback runs only after the last try fails. We tested a task with 2 retries:
Try
Result
Callback that ran
1
AirflowTaskTimeout
on_retry_callback
2
AirflowTaskTimeout
on_retry_callback
3
AirflowTaskTimeout
on_failure_callback
Choose the number by the cause. A flaky network gets 3 to 5 retries. A bug in your code fails the same way every time, so retries only delay the alert.
For a busy API, add retry_exponential_backoff=True. Each wait is then longer than the one before, up to max_retry_delay.
Watch out
A retry runs the whole task again. If the task adds rows or sends an email, a retry adds the rows again or sends the email again.
03 · Time limits
Time limits, and which one stops the run
⏱ 5 min · Topic 3 of 8
Airflow has several time limits, and each one covers something different:
Setting
It limits
When it is reached
execution_timeout
One try of one task.
That try fails, and a retry may start.
dagrun_timeout
The whole DAG run.
The run fails.
timeout on a sensor
How long the sensor waits (chapter 6).
The sensor fails, or skips with soft_fail.
execution_timeout limits one try, not the task. Take execution_timeout of 30 minutes, 3 retries and a retry_delay of 10 minutes.
The worst case is 4 tries of 30 minutes, plus 3 waits of 10 minutes. That is 150 minutes before the task finally fails.
Only dagrun_timeout limits the whole run. Set it on every production DAG, so no run stays open forever.
04 · Alerts
Callbacks, and what replaced SLAs
⏱ 5 min · Topic 4 of 8
Callbacks run when a task reaches a state. Choose the one that fits the problem:
Callback
Runs when
Use it to
on_failure_callback
The last try failed.
Alert a person.
on_retry_callback
A try failed, and a retry will follow.
Count retries. Do not wake anyone.
on_success_callback
The task succeeded.
Record a finish time.
on_skipped_callback
The task was skipped.
Notice when an important path did not run.
Callbacks do not help when a run is slow but not failing. Airflow 2 had SLAs for that. Airflow 3.0 removed them, and Airflow 3.1 added deadline alerts instead.
Alert if the run is not done 2 hours after it was queuedworked example
Python
from airflow.sdk.definitions.deadline import AsyncCallback, DeadlineAlert, DeadlineReference
from shop.alerts import tell_team # an async function
@dag(
...,
dagrun_timeout=timedelta(hours=4),
deadline=DeadlineAlert(
reference=DeadlineReference.DAGRUN_QUEUED_AT,
interval=timedelta(hours=2),
callback=AsyncCallback(tell_team),
),
)
The triggerer runs tell_team, so it must be an async function in a module the triggerer can import. Other references are DAGRUN_LOGICAL_DATE and FIXED_DATETIME.
The alert comes after 2 hours, while the run is still going. The hard stop comes after 4 hours, from dagrun_timeout.
Watch out
Old code with sla= still parses in Airflow 3.1. It only prints a warning, and no SLA alert ever comes. Search for sla= when you upgrade.
05 · Past days
Running past days without breaking the source
⏱ 5 min · Topic 5 of 8
A backfill creates runs for past dates. Use it when those runs never existed, for example for a new DAG, or after catchup=False skipped some days (chapter 5).
In Airflow 3, the CLI only creates the backfill. The scheduler then runs it, with the same pools and limits as normal runs:
Backfill three days, one run at a timeworked example
Add --dry-run first, to see the dates without creating runs. Commands from Airflow 2 tutorials, such as airflow dags backfill, are different.
--reprocess-behavior decides what happens to dates that already have a run. In our test, 2026-01-12 already had a successful run:
--reprocess-behavior
What happened
none (default)
New runs for 01-10 and 01-11. 01-12 was skipped: already exists.
failed
Also runs again the dates whose run failed.
completed
All three dates ran again, including the successful 01-12.
Watch out
Ninety days with no limit can send ninety runs to the shop database at once. Start with --max-active-runs 1, and watch the source before you raise it.
06 · Repair
Choosing the right repair
⏱ 4 min · Topic 6 of 8
Each problem has its own repair. Pick it from what you see:
What you see
Repair
Only if
One task failed, nothing after it needs to run.
Clear the task.
—
A task failed, and the tasks after it are upstream_failed.
Clear with downstream.
The tasks are safe to run twice.
Runs are missing for some days.
Backfill those dates.
You set --max-active-runs.
Runs are green, but the data is wrong.
Deploy the fix, then clear the range or backfill with completed.
The tasks are safe to run twice.
The work was done outside Airflow.
Mark success.
You can name what did the work.
Clear repairs runs that exist. Backfill creates runs that do not exist. Mark success only records a fact, so use it only when the fact is true.
Marking a failed task as success hides the failure. The tasks after it run on data that was never made.
07 · Recap
Recap
⏱ 4 min · Topic 7 of 8
Try, stop, tell, repair:
Retries for causes that pass. No retries for bugs or for tasks that email.
execution_timeout limits one try. dagrun_timeout limits the whole run.
on_failure_callback fires after the last try. Deadline alerts replace SLAs.
Backfill with --max-active-runs, and choose --reprocess-behavior on purpose.
Clear for runs that exist, backfill for runs that do not.
Check yourself: execution_timeout is 20 minutes, retries is 2, and retry_delay is 5 minutes. What is the worst case? (Answer: 3 x 20 + 2 x 5 = 70 minutes.)
08 · Next Chapter
Next Chapter
Most repairs in this chapter run tasks again.
⏱ 3 min · Topic 8 of 8
Next chapter
Idempotency & Safe Task Design
Most repairs in this chapter run tasks again.
Next, you make tasks safe to run twice, so a retry, a clear or a backfill gives the same result.