◐ ai-teacher

🔀 Pipelines & Orchestration · Chapter 8

Orchestrating failure: retries, alerting, SLAs

Your pipeline will fail. The only design choice is whether it fails loudly at 02:10 to the right person with a runbook — or quietly, discovered by the CFO at the 10 am review.

Study this lesson free in the app →

The basics

Orchestrating failure means deciding, in advance, what a pipeline does when something goes wrong: which errors it retries, which it fails fast on, who gets alerted and how urgently, and what promise — an SLA — it makes about data being fresh and correct by a certain time.

The problem it solves: a pipeline with no failure design either pages someone for every blip (so alerts get ignored) or for nothing (so bad data reaches dashboards). Both end the same way: people stop trusting the data.

The mental model: a hospital emergency ward. Triage first — a cough isn't a heart attack. Minor problems are treated on the spot (retry). Serious ones go straight to a specialist (fail fast + page). Everything has a target time to treatment (SLA). And if a patient didn't arrive who was expected, someone notices (dead man's switch).

Words you must know 5

Transient vs permanent error
heals on retry vs never will
Backoff with jitter
wait longer each retry, randomised
SLA / SLO
the promise to users / the internal target you measure
Freshness
how old the newest data in a table is
Write-audit-publish
validate data before consumers can see it

Explained

At Meghalaya Cabs, driver_payouts must be correct by 08:00 IST — 6,000 drivers check the app for their money.

One night the bank API times out, a schema changed upstream, and the scheduler restarted. Retries should handle the first, an immediate page the second, and a freshness alarm the third. Most teams only handle the first.

The railway station master

A station master doesn't panic when one train is 3 minutes late — trains recover time. He doesn't wait politely when a signal has failed either; he calls the engineer at once.

He has a board: the 7:40 to Guwahati must leave by 7:55. He doesn't watch each train's engine — he watches the board.

And the most important alarm on his desk: no train has passed in 40 minutes. Nothing failed, nothing broke — something didn't happen. That's the alarm most pipelines don't have.

What happens when a task fails

Every arrow is a decision you should make at design time, not at 02:10.

Retries done right

Only retry what can heal: timeouts, 5xx, HTTP 429 (honour Retry-After), deadlocks, preemptible-node loss. Never retry auth failures, schema mismatches or validation failures — raise AirflowFailException and alert.

Exponential backoff with jitter: 1 min, 2, 4, 8… each randomised. Without jitter, 300 tasks that failed together retry together and knock the recovering API over again (thundering herd).

Cap the total: max_retry_delay and a sane retries so the whole retry schedule fits inside your SLA. Five retries with 30-minute backoff is a 15-hour delay.

Retries require idempotency (partition overwrite, MERGE, idempotency keys on external calls like payouts) — otherwise each retry duplicates.

Retry at the right level: retry the API call inside the task for sub-second blips; retry the task for minute-scale outages; re-run the DAG for multi-hour incidents.

Failure handling in Airflow

TRANSIENT = (requests.Timeout, requests.ConnectionError)

@task(retries=4, retry_delay=timedelta(minutes=2),
      retry_exponential_backoff=True, max_retry_delay=timedelta(minutes=20),
      execution_timeout=timedelta(minutes=30))
def pay_drivers(ds=None):
    batch = load_batch(ds)
    if not batch.validate():                    # bad data: retry won't help
        raise AirflowFailException(f'invalid payout batch {ds}')
    try:
        bank.submit(batch, idempotency_key=f'payout-{ds}')
    except bank.AuthError as e:                 # permanent
        raise AirflowFailException(str(e))
    # TRANSIENT errors propagate → Airflow retries with backoff

@dag(..., default_args={'on_failure_callback': page_if_critical},
     dagrun_timeout=timedelta(hours=4))

The idempotency_key is what makes retrying a payment safe: the bank recognises the second submit and doesn't pay twice.

SLAs for data: what to promise and measure

Data SLIs come in three kinds: freshness (newest data age), completeness (rows vs source / expected), correctness (tests passing, totals reconcile).

Measure at the table, not the task. A green DAG that loaded zero rows is a missed SLA. Check max(loaded_at) and counts directly — dbt source freshness, a final check task, or an observability tool (Elementary, Monte Carlo, Soda).

Airflow's own sla= (2.x) is measured from the run's scheduled time, ignores manual runs, and is notoriously quirky; Airflow 3 removed it in favour of deadline alerts. Most teams measure freshness outside the orchestrator.

Alert routing at Meghalaya Cabs

signal                          severity   route            when
──────────────────────────────────────────────────────────────────
payouts not published by 07:30  P1         page on-call     now
  (SLA 08:00)                                 + #payouts
fact_rides freshness > 3 h      P2         page, business   now
                                           hours only
unique test failed on mart      P2         block publish +  now
                                           Slack owner
marketing report DAG failed     P3         Slack channel    next day
single retry succeeded          —          no alert,        dashboard
                                           count it         only
no DAG run at all since 02:00   P1         dead man's       now
                                           switch → page

Rule: every page must be actionable and link a runbook. If the right response is "ignore it", it shouldn't be an alert.

At scale

Alert fatigue is the real outage. At 3,000 tasks a night, alerting on every failure means 40 Slack messages before breakfast and the one that matters scrolls past. Alert on SLA risk and data impact, not on task failures that retried successfully.

Dead man's switch. Failure callbacks can't fire if the scheduler is down, the DAG is paused or never got scheduled. Have an external check (e.g. a heartbeat ping each run to a monitoring service, or a freshness query from outside Airflow) that alerts when expected success didn't arrive.

Write-audit-publish. Write to a staging table/branch, run tests, then atomically swap/publish (Iceberg branches, Delta, a view swap). Failed audits never reach dashboards — consumers see yesterday's correct data, labelled with its age, instead of today's wrong data.

Error budgets. A 99% SLO allows ~3–4 missed days a year. Burn it fast and you prioritise reliability work over new pipelines — that's how SLOs change behaviour.

Words to know

Transient errorA failure likely to succeed on retry, such as a timeout or HTTP 503.
Exponential backoff with jitterRetry delays that grow each attempt and are randomised to avoid synchronized retries.
SLAThe promise made to data consumers, such as a table being correct by 08:00.
SLOThe internal reliability target for an SLA, such as meeting it on 99% of days.
Data freshnessHow old the most recent data in a table is relative to now.
Dead man's switchAn alert that fires when an expected success signal doesn't arrive.
Write-audit-publishWriting to a hidden location, testing it, and only then exposing it to consumers.

In two minutes

Test yourself

1. Which error should NOT be retried?

  1. HTTP 503 from the bank API
  2. A socket timeout
  3. HTTP 429 rate limited
  4. Warehouse authentication failed after a password rotation
Show the answer

Warehouse authentication failed after a password rotation — Auth failures won't heal on their own; retrying only delays the alert. The others are transient and retry well (honour Retry-After for 429).

2. Why add jitter to exponential backoff?

  1. So many tasks that failed together don't all retry at the same instant and overload the recovering service
  2. To make retries faster
  3. To make logs readable
  4. Airflow requires it
Show the answer

So many tasks that failed together don't all retry at the same instant and overload the recovering service — Synchronised retries create a thundering herd. Randomising delays spreads the load.

3. Payouts must be done by 08:00; the task starts at 06:00 and takes 40 min. Retry config: retries=6, backoff doubling from 15 min. Problem?

  1. None, more retries are safer
  2. The retry schedule (15+30+60+… minutes) runs far past 08:00, so the SLA is missed while 'retrying'
  3. Backoff should be linear
  4. Retries should be 0
Show the answer

The retry schedule (15+30+60+… minutes) runs far past 08:00, so the SLA is missed while 'retrying' — Size retries so the whole schedule fits the SLA window, then escalate. Here the delays alone total over 15 hours.

4. A DAG ran green but fact_rides loaded 0 rows because the source export was empty. Which practice would have caught it?

  1. More retries
  2. A longer execution_timeout
  3. Measuring completeness on the table (row count vs expected/source) as an SLI
  4. Poke-mode sensors
Show the answer

Measuring completeness on the table (row count vs expected/source) as an SLI — Task success ≠ data success. Check the data: counts, freshness and reconciliation against the source.

5. Why make the bank submission use idempotency_key=f'payout-{ds}'?

  1. So a retry after an ambiguous timeout doesn't pay drivers twice
  2. To speed up the API
  3. To encrypt the request
  4. Banks require unique keys per retry
Show the answer

So a retry after an ambiguous timeout doesn't pay drivers twice — A timeout doesn't tell you if the payment went through. The key lets the bank recognise the retry and return the original result.

6. The scheduler crashed at 01:00 and no DAGs ran. No alert fired. What was missing?

  1. on_failure_callback on every task
  2. A dead man's switch — an external check that expected success arrived
  3. More retries
  4. A larger worker pool
Show the answer

A dead man's switch — an external check that expected success arrived — Callbacks run inside Airflow; if nothing runs, nothing calls them. Something outside must notice the silence.

There are 10 questions, 3 puzzles and 5 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →