Task Retries & Error Handling in Apache Airflow
Building Resilient DAGs That Survive Failures ππ‘οΈβ
The Story: Failures Are Normal β Fragile Pipelines Are Notβ
In the real world, data pipelines fail.
- APIs timeout
- Databases briefly go down
- Networks flicker
- Cloud services throttle requests
A fragile DAG fails once and stops.
A resilient DAG retries, recovers, and completes.
Apache Airflow is designed with this reality in mind.
Thatβs why task retries and error handling are first-class features.
This article explains how retries work, how to design intelligent error handling, and how to avoid common retry disasters in production.
What Is a Task Retry in Airflow?β
A retry is Airflowβs way of saying:
βThis task failed β letβs try again after some time.β
Retries help recover from temporary or external failures, without human intervention.
π Important:
Retries happen at the task level, not the DAG level.
When Should You Use Retries?β
Good Use Cases β β
- API rate limits
- Temporary network issues
- Short database outages
- Cloud service hiccups
Bad Use Cases ββ
- Invalid SQL
- Buggy Python code
- Missing files
- Logic errors
Retries fix instability, not bad code.
Core Retry Parameters You Must Knowβ
Airflow provides multiple retry controls.
| Parameter | Description |
|---|---|
retries | Number of retry attempts |
retry_delay | Wait time before retry |
retry_exponential_backoff | Increases delay per retry |
max_retry_delay | Upper limit for delay |
Basic Retry Exampleβ
from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime, timedelta
def unstable_task():
raise Exception("Temporary failure")
with DAG(
dag_id="retry_example",
start_date=datetime(2024, 1, 1),
schedule_interval="@daily",
catchup=False,
) as dag:
task = PythonOperator(
task_id="unstable_task",
python_callable=unstable_task,
retries=3,
retry_delay=timedelta(minutes=5),
)
Inputβ
| Parameter | Value |
|---|---|
| Retries | 3 |
| Retry delay | 5 minutes |
| Initial attempt | Fails |
Outputβ
| Attempt | Result |
|---|---|
| Try 1 | Failed |
| Try 2 | Failed |
| Try 3 | Failed |
| Try 4 | Final failure |
π Total attempts = 1 initial + retries
Retry State Transitionsβ
RUNNING β FAILED β UP_FOR_RETRY β RUNNING
After retries are exhausted:
FAILED (final)
This state is visible in:
- Grid View
- Task Instance details
- Logs
Exponential Backoff β Smarter Retriesβ
Retrying too fast can make things worse.
Exponential backoff increases delay after each failure.
Exponential Backoff Exampleβ
task = PythonOperator(
task_id="api_call",
python_callable=unstable_task,
retries=5,
retry_delay=timedelta(minutes=1),
retry_exponential_backoff=True,
max_retry_delay=timedelta(minutes=30),
)
Inputβ
| Retry | Delay |
|---|---|
| 1 | 1 min |
| 2 | 2 min |
| 3 | 4 min |
| 4 | 8 min |
| 5 | 16 min |
Outputβ
- Reduced pressure on external systems
- Higher recovery success
- Fewer cascading failures
π Always use exponential backoff for APIs.
Error Handling Inside Tasksβ
Retries only work if the task fails correctly.
Correct Failure Patternβ
def process_data():
if not data_available():
raise Exception("Data not ready")
Incorrect Pattern ββ
def process_data():
try:
risky_call()
except Exception:
print("Error ignored")
π If Airflow doesnβt see an exception, it assumes success.
Failure Callbacks β Controlled Reactionsβ
Retries handle recovery.
Callbacks handle communication.
Failure Callback Exampleβ
def notify_failure(context):
dag_id = context["dag"].dag_id
task_id = context["task_instance"].task_id
print(f"Task failed: {dag_id}.{task_id}")
task = PythonOperator(
task_id="critical_task",
python_callable=unstable_task,
retries=2,
on_failure_callback=notify_failure,
)
Inputβ
| Event | Value |
|---|---|
| Final retry | Failed |
Outputβ
Task failed: retry_example.critical_task
π Callbacks are triggered only after retries are exhausted.
Retry vs SLA β Know the Differenceβ
| Feature | Retry | SLA |
|---|---|---|
| Purpose | Recovery | Detection |
| Changes task state | Yes | No |
| Delays execution | Yes | No |
| Triggers alerts | Indirect | Yes |
π Use retries to fix, SLAs to observe.
Clearing Tasks vs Retriesβ
Retries are automatic.
Clearing is manual.
| Action | Use Case |
|---|---|
| Retry | Temporary issue |
| Clear task | Code fix or data fix |
π Clearing a task creates a new task instance attempt.
Input & Output β Realistic Retry Scenarioβ
Inputβ
- API task
- Retries: 4
- Exponential backoff
- Max delay: 20 minutes
Output Timelineβ
| Attempt | Delay | Result |
|---|---|---|
| 1 | β | Failed |
| 2 | 2 min | Failed |
| 3 | 4 min | Success |
| 4 | β | Not executed |
β DAG completes successfully without manual action.
Common Retry Mistakesβ
β Retrying non-recoverable errors
β No retry delay
β Excessive retries blocking downstream tasks
β Ignoring failure callbacks
β Treating retries as monitoring
Best Practices for Productionβ
β
Use retries only for transient failures
β
Prefer exponential backoff
β
Keep retry counts reasonable (2β5)
β
Always raise exceptions on failure
β
Combine retries with alerts
Summary π§ β
- Failures are normal in distributed systems
- Retries make DAGs resilient
- Exponential backoff prevents overload
- Proper error handling enables retries
- Callbacks ensure visibility
Key Takeawaysβ
- β Retries recover from temporary failures
- β Exceptions drive Airflow state
- β Exponential backoff is critical
- β Callbacks notify humans
- β Resilience beats perfection
Whatβs Next? πβ
β‘οΈ Unit Testing Airflow DAGs
(How to test DAG structure, tasks, and logic using pytest and Airflow testing utilities)