Home DevOps Airflow Deferrable Operators: Cut Sensor Costs to Zero
Advanced 3 min · September 04, 2026
Airflow Deferrable Operators and Triggerer

Airflow Deferrable Operators: Cut Sensor Costs to Zero

Airflow deferrable operators free worker slots on long sensor waits.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
Production
production tested
September 04, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 25 min
  • A DAG with at least one sensor (file, S3, or external task)
  • An Airflow 3.x deployment where you can run the triggerer
  • Basic understanding of worker slots and pools
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Airflow deferrable operators release the worker slot while waiting, letting the async triggerer watch the condition instead
  • Key components: deferrable=True on supported sensors, the triggerer process, deferred task state, and resume callbacks
  • Performance insight: a 40-minute poke wait costs a full slot for 2,400 seconds; deferred it costs near zero, so 50 concurrent waits fit in one triggerer
  • Production insight: one team's file sensor starved tier-1 tasks for 40 minutes because poke mode sleeps on a worker slot
  • Biggest mistake: going deferrable without running or scaling the triggerer, parking tasks in deferred state with nobody watching
✦ Definition~90s read
What is Airflow Deferrable Operators and Triggerer?

Airflow deferrable operators are sensors and operators that suspend instead of occupying a worker slot, letting the async triggerer watch the condition. Passing deferrable=True cuts long-wait costs to near zero.

Imagine hiring a full-time employee to sit by a mailbox for 40 minutes waiting for one letter, instead of leaving a doorbell that rings when it arrives.
Plain-English First

Imagine hiring a full-time employee to sit by a mailbox for 40 minutes waiting for one letter, instead of leaving a doorbell that rings when it arrives. Poke sensors are the employee; deferrable sensors are the doorbell, and the triggerer is the person who hears all the doorbells at once.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

Worker slots are expensive. Every sensor sleeping in poke mode rents one by the minute while doing absolutely nothing. Multiply by dozens of waiting sensors and your cluster works hard at standing still.

One file sensor held a full slot for 40 minutes waiting on data that arrived late. Real tasks queued behind it, SLAs slipped, and the fix cost one keyword argument. You'll learn which one.

Deferrable operators hand the waiting to the triggerer and free the slot. Pennies instead of dollars. Let's do the math.

The Poke Problem: Worker Slot Economics

A poke sensor rents a worker slot and naps on it. Between checks it sleeps, but the slot stays reserved, billed, and unavailable to real work. A one-second check repeated for 40 minutes costs what 40 minutes of compute costs.

You'll feel it as phantom load. Worker occupancy reads 100%, CPU reads 15%, and ready tasks queue behind sensors that are technically running. Adding workers helps briefly, then the new slots fill with more napping sensors.

Price waits in slot-minutes. One 40-minute poke wait burns 40 slot-minutes; fifty concurrent ones burn 2,000. That single number usually ends the debate about whether sensor modes matter.

📊 Production Insight
Occupancy 100% with CPU 15% means sensors napping on slots. Slot-minutes price the waste. Rule: any wait over 5 minutes leaves poke mode.
🎯 Key Takeaway
Poke sensors bill a full slot for the whole wait while napping between checks. Count slot-minutes per wait and the cost of sleeping becomes undeniable.

Deferrable Model: Defer Plus Resume

Deferrable flips the model. The task suspends itself, releases its worker slot, and registers an async trigger. When the condition fires, the task resumes through the queue like any other work. Waiting moves from workers to the triggerer.

You'll convert with one argument. deferrable=True on supported sensors plus a running triggerer is the whole migration for most waits. The Grid view shows the honest deferred state instead of a fake running one.

Keep the safety rails. Timeouts still bound the wait, poke_interval still spaces trigger checks, and mode='reschedule' remains a sane fallback. Deferral changes where waiting happens, not whether deadlines apply.

Flip fleets with one setting when you're ready. [operators] default_deferrable (env AIRFLOW__OPERATORS__DEFAULT_DEFERRABLE) makes every sensor/operator that supports both modes defer by default, so deferrable=True stops being a per-task negotiation. Override per task where a tight local poke still makes sense. And know the two worker-skipping upgrades since 2.10: start_from_trigger defers straight to the triggerer without ever renting a worker, and end_from_trigger finishes there too — no resume slot at all for pure waits.

dags/landing_guard.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import datetime
import pendulum
from airflow.sdk import dag, task
from airflow.providers.amazon.aws.sensors.s3 import S3KeySensor

@dag(
    dag_id="landing_guard",
    schedule="*/15 * * * *",
    start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
    catchup=False,
    tags=["sensors", "landing"],
)
def landing_guard():
    wait_for_drop = S3KeySensor(
        task_id="wait_for_drop",
        bucket_name="acme-landing",
        bucket_key="sales/{{ ds }}/_SUCCESS",
        deferrable=True,  # free the slot; triggerer watches instead
        poke_interval=60,
        timeout=datetime.timedelta(hours=2),
        mode="reschedule",  # fallback path if triggerer is down
    )

    @task
    def promote() -> str:
        return "drop present, promoting"

    wait_for_drop >> promote()

landing_guard()
📊 Production Insight
Deferred state is honest waiting; fake-running is hidden cost. One argument migrates most sensors. Rule: timeouts stay mandatory after deferral.
🎯 Key Takeaway
Defer releases the slot, the trigger watches, resume re-queues the task. One keyword plus a running triggerer converts most long waits.

The Triggerer Process and Its Scaling

The triggerer is one async event loop watching thousands of conditions. It costs a fraction of a worker and replaces whole fleets of napping sensors. You'll run it as a first-class component, not an afterthought.

Scale it on backlog, not vibes. The queued-trigger count and resume latency tell you when one triggerer saturates; a second triggerer or bigger host follows the same playbook as scheduler scaling. Idle triggers are cheap, simultaneous resume bursts are the load to plan for.

Monitor it like workers. Heartbeat, backlog, resume latency, and failure rate each get a dashboard panel and one alert. A dead triggerer parks every deferred task silently, which recreates the exact outage deferrable was bought to prevent.

Size it with the real knob: triggerer capacity defaults to 1000 concurrent triggers per process (config triggerer capacity, env AIRFLOW__TRIGGERER__DEFAULT_CAPACITY). Run two-plus replicas for HA like schedulers — triggers survive in the metadata DB across restarts, but nothing resumes while zero triggerers run. Watch the triggerer heartbeat like any component, and use queues_enabled with --queues on the triggerer hosts when teams need isolated trigger lanes.

📊 Production Insight
Idle triggers cost little; 500 simultaneous resumes cost a lot. Backlog over 500 means scale now. Rule: monitor the triggerer like a worker pool.
🎯 Key Takeaway
One async loop replaces fleets of napping sensors, but it needs backlog monitoring and burst planning like any other component. Dead triggerer, parked fleet.

Converting Sensors With Deferrable True

Conversion is mechanical. Find sensors with long timeouts in the DAG folder, add deferrable=True where the provider supports it, and confirm the deferred state on the next run. You'll batch the change per team to keep reviews small.

Verify provider support first. Most file, S3, and external-task sensors support deferrable in current providers; exotic ones may not yet. Unsupported sensors move to mode='reschedule' as the interim fix, not eternal poke.

Gate it in review. Any new sensor with an expected wait over 5 minutes must justify poke mode in the pull request. Defaults drift back to poke without the rule written down.

💡The 5-Minute Conversion Rule
Convert every poke-mode wait over 5 minutes to deferrable in your next review pass. Under 2 minutes, poke stays simpler. Between 2 and 5, reschedule is the honest middle.
📊 Production Insight
Migration is grep plus one keyword per sensor. Review gates prevent regression. Rule: new waits over 5 minutes justify poke or go deferrable.
🎯 Key Takeaway
Grep for long-timeout sensors, add deferrable where supported, reschedule where not. Write the 5-minute rule into review checklists so poke doesn't creep back.

Cost Math: 40-Minute Poke Versus Zero

The incident math is stark. One 40-minute poke wait costs 40 slot-minutes; deferred it costs under one. Fifty concurrent waits cost 2,000 slot-minutes poking versus a single triggerer's spare capacity deferred. That's a whole worker fleet versus a sidecar.

You'll translate it to money per quarter. Slot-minutes times worker cost per minute times waiting sensors per day compounds into real budget. Teams that price it once never go back to poke for long waits.

Count the triggerer honestly. It needs its host, its monitoring, and burst headroom. Even fully loaded it costs an order of magnitude less than the slots it frees, but free is a lie and budgets deserve the truth.

scripts/triggerer-ops.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
# run the triggerer alongside the scheduler (one per cluster to start)
airflow triggerer &
ps aux | grep -i triggerer | grep -v grep

# how many tasks currently wait on the triggerer?
# (metadata DB check during business hours)
PG="postgresql://airflow:***@db:5432/airflow"
psql "$PG" -c "SELECT state, count(*) FROM task_instance WHERE state IN ('deferred','scheduled') GROUP BY 1;"

# cost math for the review doc
python3 -c "print('poke cost per 40-min wait: 1 slot x 40 min =', 40, 'slot-minutes')"
python3 -c "print('deferred cost per 40-min wait: ~0 slots + triggerer share =', '<1', 'slot-minute')"
📊 Production Insight
Slot-minutes convert architecture taste into budget numbers. 2,000 vs near-zero ends debates. Rule: show the quarterly math once per team.
🎯 Key Takeaway
Fifty concurrent 40-minute waits cost 2,000 slot-minutes poking, one triggerer deferred. Price it per quarter and poke never wins long waits again.

Timeouts and Resume Semantics

Timeouts bound every wait, deferred or not. A file that never lands must fail the task, page the owner, and free the DAG run. Without a timeout the run parks in deferred forever and the SLA misses with no failure to investigate.

Resume semantics reward idempotent downstream tasks. A resumed task re-enters the queue and may run on a different worker minutes later; anything it touches must tolerate that gap. You'll design post-sensor tasks like post-retry tasks.

Test the deadline path. Set a one-minute timeout on staging, run the sensor against a missing key, and confirm it fails loudly with the right alert. Untested timeouts are wishes, and wishes don't page.

Write triggers with triggerer rules in mind. Run() must be an async generator yielding TriggerEvent — and every client inside it must be async (aiohttp, aiobotocore), never blocking SDK calls, or one trigger stalls the whole event loop. Keep constructor args JSON-serializable or defer blows up at serialize time. Put external cancellation (kill that BigQuery job, terminate that Databricks run) in on_kill, which fires on explicit user kills but never on triggerer restarts or rolling deploys — cleanup-style logic there would cancel in-flight work mid-deploy.

📊 Production Insight
Missing files park deferred runs forever without timeouts. Resume lands on arbitrary workers later. Rule: timeout every wait, idempotence after every resume.
🎯 Key Takeaway
Deferred waits still need deadlines, resumed tasks still need idempotent downstreams. Test the timeout path on staging or the first missing file tests it in prod.
● Production incidentPOST-MORTEMseverity: high

The Sensor That Burned a Worker Slot for 40 Minutes

Symptom
Worker occupancy hit 100% while actual CPU stayed low; the Grid view showed dozens of running sensors and a growing queue of ready tasks. A 40-minute file wait blocked tier-1 tasks behind it. SLA misses traced back to queueing, not execution, and adding workers only fed more slots to more waiting sensors.
Assumption
The team assumed sensors were cheap because each poke is trivial. A one-second file check costs nothing, so holding a slot for 40 minutes felt like holding it for 40 one-second checks. Worker capacity planning counted task throughput, never wait time. Nobody priced the sleeping.
Root cause
The sensor ran in default poke mode, which holds a worker slot for the entire wait while sleeping between checks. Long-polling waits stacked up across concurrent DAG runs, starving real tasks of slots. No triggerer ran in the deployment, so the deferrable path that would have cost nearly nothing wasn't even available.
Fix
The sensor was converted with deferrable=True and the triggerer process was brought up behind it. Waiting cost dropped from a full worker slot to a fraction of triggerer capacity, and 50 concurrent waits fit where one used to strain. Timeouts were added so missing files still fail loudly, and the runbook now flags any poke-mode wait over 5 minutes in review.
Key lesson
  • Price the wait, not the poke. A trivial check held for 40 minutes costs a full slot, and fifty such waits cost a fleet.
  • Every deferrable deployment needs triggerer capacity planning. Freeing worker slots just moves the waiting somewhere that must also be scaled.
  • Deadlines survive deferral. Timeouts and failure handling stay mandatory even when waiting becomes nearly free.
Production debug guideFour sensor-wait failures, with the exact commands that unstick each one.4 entries
Symptom · 01
Worker slots full but most running tasks are sensors
Fix
List running sensors and their modes: airflow tasks list <dag_id> then inspect durations in the Grid view. Any sensor running 10+ minutes in poke mode gets deferrable=True. Confirm the triggerer runs with ps aux | grep triggerer and watch the task enter deferred state on the next run. Check triggerer capacity and heartbeat too — deferred tasks pile silently when triggers exceed AIRFLOW__TRIGGERER__DEFAULT_CAPACITY on a single replica.
Symptom · 02
Deferred tasks resume minutes late or never
Fix
Check triggerer logs for backlog warnings and count deferred tasks: SELECT count(*) FROM task_instance WHERE state='deferred'. If the count grows while resumes lag, add triggerer capacity and cap deferrable fan-out per DAG run until the backlog drains.
Symptom · 03
A deferred sensor waits days on a file that never arrives
Fix
Inspect the sensor's timeout and soft_fail settings in the DAG file. Add timeout=datetime.timedelta(hours=2) with explicit failure handling so a missing file fails the task instead of parking the run. Rerun with airflow dags test to verify the deadline fires.
Symptom · 04
deferrable=True raises unsupported-argument errors
Fix
Check provider version for deferrable support: airflow providers list | grep -i amazon (or your sensor's provider). Upgrade the provider or switch the sensor to mode='reschedule' with a sane poke_interval until deferrable support lands.
Poke vs Reschedule vs Deferrable Compared
ModeWorker cost while waitingResume latencyBest for
Poke (default)Full slot for entire waitSeconds (already on worker)Waits under 2 minutes
RescheduleSlot freed between pokesMinutes (re-queued each poke)Waits of minutes to an hour
Deferrable + triggererNear zero; triggerer watchesSeconds on resume eventWaits over 5 minutes, high fan-out
Deferrable at 500+ fan-outNear zero; triggerer may queueMinutes if triggerer saturatedOnly with scaled triggerer capacity
default_deferrablePer-task flag drift across a large fleetFleet default True, override per tasktasks
start_from_triggerPaying worker slots just to start waitingDefer before first worker pickuptasks
Triggerer capacityDeferred tasks piling past one process1000 per process, 2+ replicastasks
⚙ Quick Reference
2 commands from this guide
FileCommand / CodePurpose
dagslanding_guard.pyfrom airflow.sdk import dag, taskDeferrable Model
scriptstriggerer-ops.shairflow triggerer &Cost Math

Key takeaways

1
Defer releases the worker slot for the whole wait
a 40-minute poke drops from full slot cost to milliseconds, with only defer and resume events.
2
Ship the triggerer in the same commit as deferrable tasks (capacity 1000 per process, two-plus replicas), or deferred tasks park forever.
3
Convert with deferrable=True plus sane poke_interval and timeout, or flip [operators] default_deferrable fleet-wide and override per task.
4
For pure waits, use start_from_trigger and end_from_trigger to skip worker slots entirely on both ends of the wait.
5
Write async-only triggers with serializable args, and put external cancellation in on_kill
never in paths that run on triggerer restarts.

Common mistakes to avoid

4 patterns
×

Leaving long-wait sensors in default poke mode

Symptom
A 40-minute file wait holds a full worker slot at 100% occupancy; real tasks queue behind a sensor doing nothing but sleeping.
Fix
Pass deferrable=True on long-wait sensors and confirm the triggerer process runs (airflow triggerer). Deferred tasks show the deferred state and resume automatically; no worker slot burns while they wait.
×

Going deferrable without running the triggerer

Symptom
Deferred tasks pile in deferred state forever; the triggerer backlog grows and resume latency stretches from seconds to tens of minutes.
Fix
Run one triggerer per cluster, watch its queued-trigger backlog metric, and add capacity before the backlog passes 500. The triggerer is cheap; starving it re-creates the slot problem one layer down.
×

Deferring with no timeout because waiting is now free

Symptom
DAG runs sit deferred for days on a file that will never land; downstream SLAs miss with no task ever failing.
Fix
Set timeout plus soft_fail or explicit failure handling on every deferrable sensor. A deferred wait still needs a deadline, or a file that never arrives parks the DAG run indefinitely.
×

Assuming deferred tasks resume instantly at any scale

Symptom
500 deferred sensors wake simultaneously and the single triggerer takes 20 minutes to resume them all; SLAs miss in a thundering herd.
Fix
Size triggerer capacity for resume bursts: hundreds of deferred sensors resume at once when an upstream dataset lands. Load-test the burst on staging and cap deferrable fan-out per DAG run. Audit trigger clients for blocking SDK calls (swap to aiohttp/aiobotocore) and confirm constructor args serialize — both fail silently until defer time.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Compare poke, reschedule, and deferrable sensor modes including costs.
Q02SENIOR
What does the triggerer do and how do you scale it?
Q03JUNIOR
What does the deferred task state mean?
Q01 of 03SENIOR

Compare poke, reschedule, and deferrable sensor modes including costs.

ANSWER
Poke holds a worker slot for the full wait; reschedule frees the slot between pokes but re-queues each interval; deferrable releases the slot entirely while an async trigger on the triggerer watches the condition. The 40-minute sensor burned a slot because poke mode sleeps on a worker. Deferrable drops wait cost to near zero but requires running and scaling the triggerer, plus timeouts so deferred waits still have deadlines.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
How do I convert a sensor to deferrable?
02
What is the triggerer process?
03
How big is the cost difference, really?
04
Do all sensors support deferrable mode?
05
How do I know the triggerer needs scaling?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
Verified
production tested
September 04, 2026
last updated
1,750
articles · all by Naren
🔥

That's Airflow. Mark it forged?

3 min read · try the examples if you haven't

Previous
Airflow Data Quality Gates
33 / 37 · Airflow
Next
Airflow Object Storage Workflows