Home DevOps Airflow Sensors Exposed: 4,320 Pokes and No File Found
Intermediate 3 min · September 04, 2026

Airflow Sensors Exposed: 4,320 Pokes and No File Found

Airflow FileSensor poked 4,320 times holding a worker slot.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.

Follow
Production
production tested
September 04, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 30 min
  • Basic DAG authoring with TaskFlow
  • Understanding of worker slots and Pools
  • Access to a landing bucket or SFTP drop
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Airflow sensors are operators that wait for an event like a file, table, or upstream DAG before letting downstream run
  • Key components are poke_interval, timeout, mode poke vs reschedule, soft_fail, and deferrable operators with the triggerer
  • Performance insight: poke mode held a worker slot for 12 hours across 4,320 pokes; reschedule mode frees the slot between checks
  • Production insight: always set timeout and prefer reschedule or deferrable for waits over 5 minutes to avoid slot starvation
✦ Definition~90s read
What is Airflow Sensors?

A sensor is an operator that waits for an event and succeeds when it appears, with poke and reschedule modes controlling worker slot usage.

A sensor is like waiting by the door for a package instead of going about your day and checking every hour.
Plain-English First

A sensor is like waiting by the door for a package instead of going about your day and checking every hour. If you stand by the door all day you cannot do anything else, but if you check and go back to work you stay productive. Airflow sensors work the same way: poke mode stands by the door holding a worker, while reschedule mode checks and frees the worker until the next check.

Your FileSensor poked 4,320 times overnight and the file never arrived. It also held a worker slot the whole time, so real tasks queued behind a wait.

Sensors are not free. You'll learn when to poke, when to reschedule, and when to defer so waits stop costing you workers.

We cover the sensor catalog, timeout math, and the custom sensor pattern that keeps prod calm. You'll set timeouts on every sensor after this.

Waiting should be cheap. Make it so.

What a Sensor Is: An Operator That Waits

A sensor succeeds when its condition appears and fails when its timeout expires. Until then it pokes on an interval you control.

Use sensors for files, partitions, upstream DAGs, and API readiness. Do not use them as polling ETL loops; they wait, they do not extract.

dags/ledger_daily.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from airflow.decorators import dag, task
from airflow.sensors.filesystem import FileSensor
from datetime import datetime

@dag(schedule="@daily", start_date=datetime(2026, 1, 1), catchup=False, tags=["ledger"])
def ledger_daily():
    wait_file = FileSensor(
        task_id="wait_vendor_file",
        filepath="/data/landing/vendor_ledger.csv",
        poke_interval=300,
        timeout=21600,
        mode="reschedule",
        soft_fail=False,
    )

    @task
    def load_ledger():
        return {"rows": 88000}

    wait_file >> load_ledger()

ledger_daily()
📊 Production Insight
Explicit timeout turns hangs into alerts.
No timeout means silent all-night waits.
Rule: every sensor gets a timeout.
🎯 Key Takeaway
Sensors wait, then succeed or fail.
Timeouts make waits safe.
Set one always.

Poke vs Reschedule: Worker Slot Economics

Poke holds a worker slot for the entire wait and sleeps between checks. Reschedule frees the slot between checks and re-queues the poke.

Short waits under a minute can poke. Anything longer should reschedule. A 12-hour poke at 10-second intervals burned 4,320 slot-minutes for nothing.

Latency decides the mode: checks every few seconds belong in poke, checks every minute or slower belong in reschedule. Reschedule with a poke_interval under about 5 minutes can swamp the scheduler with rapid re-queues, so lengthen the interval instead of shrinking it. For waits past an hour, add deferrable True where the sensor supports it and the Triggerer watches while workers stay free. Set exponential_backoff True with max_wait to back off polling against flaky vendors instead of hammering them at a fixed cadence.

📊 Production Insight
Reschedule cut slot usage to near zero.
Poke blocked revenue tasks all night.
Rule: reschedule past 5 minutes.
🎯 Key Takeaway
Poke holds, reschedule frees.
Long waits must reschedule.
Slots cost money.

Timeout and Soft Fail: Fail vs Skip vs Keep Waiting

timeout caps the total wait from first poke to deadline. When it expires the sensor fails unless soft_fail is true, which marks it skipped.

Use fail for money-path inputs where missing data must page. Use soft_fail for optional partitions where downstream should skip gracefully.

The stock timeout is seven days, which is a trap; always set an explicit deadline per dataset. timeout counts from the first poke attempt, not per retry, and retries restart parts of that accounting, so a sensor with generous retries can outlive its timeout across tries. Don't confuse timeout with execution_timeout: in reschedule mode each poke attempt is short, so execution_timeout barely bites while timeout governs the whole wait. Two more failure flavors exist: silent_fail logs poke exceptions and keeps waiting, while soft_fail converts deadline misses to skips. A proven pattern pairs soft_fail with a BranchPythonOperator on ALL_DONE: the sensor skips on timeout, the branch reads the sensor state and routes to a with-data or without-data path.

dags/ledger_optional.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
from airflow.decorators import dag, task
from airflow.sensors.filesystem import FileSensor
from datetime import datetime

@dag(schedule="@daily", start_date=datetime(2026, 1, 1), catchup=False, tags=["ledger"])
def ledger_optional():
    wait_optional = FileSensor(
        task_id="wait_marketing_export",
        filepath="/data/landing/marketing.csv",
        poke_interval=600,
        timeout=10800,
        mode="reschedule",
        soft_fail=True,
    )

    @task(trigger_rule="none_failed")
    def summarize():
        return {"status": "done"}

    wait_optional >> summarize()

ledger_optional()
# soft_fail=True turns timeout into skipped; pair with BranchPythonOperator(TRIGGER ALL_DONE)
# to route to a no-data path instead of paging at 3 AM
📊 Production Insight
soft_fail turned late files into skips.
Fail-everything paged on optional data.
Rule: fail money, skip optional.
🎯 Key Takeaway
Timeout sets the deadline.
soft_fail chooses skip over fail.
Decide per dataset.

The Common Sensor Catalog

FileSensor watches landing paths. S3KeySensor watches object keys. ExternalTaskSensor waits on another DAG run. TimeSensor waits until a clock time.

SqlSensor polls a row count or watermark. HttpSensor checks API readiness. Pick the sensor that matches the event instead of shelling out in Bash.

Rough starting points that survive prod: FileSensor at 60s poke with a 2 to 4 hour timeout, ExternalTaskSensor at 300s poke with 6 to 12 hours, HttpSensor at 30 to 60s with 30 minutes to 2 hours, S3KeySensor at 60s with 2 to 4 hours, SqlSensor at 30s poke with 30 minutes to an hour. Prefer PythonSensor over BashSensor for JSON or auth logic, and TimeDeltaSensor over sleep calls so waits stay visible in the UI. HttpSensor needs its own request timeout separate from the sensor timeout or one hung socket eats the whole deadline.

🔥Match the Sensor to the Event
Files use FileSensor or S3KeySensor, cross-DAG waits use ExternalTaskSensor, watermarks use SqlSensor. Wrong sensor means brittle poke logic and false negatives.
📊 Production Insight
SqlSensor replaced a 200-line poll loop.
Watermark checks run in 2 seconds.
Rule: use native sensors first.
🎯 Key Takeaway
Catalog covers files, keys, and DAGs.
Native beats custom scripts.
Pick by event type.

ExternalTaskSensor and Its Exactness Gotcha

ExternalTaskSensor needs the exact logical date of the upstream run. A one-hour delta mismatch means it waits forever on a run that already succeeded.

Pin external_dag_id, allowed_states, and execution_delta carefully. When producers and consumers drift, switch to asset-triggered scheduling instead of date math.

dags/ledger_consumer.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
from airflow.decorators import dag, task
from airflow.sensors.external_task import ExternalTaskSensor
from datetime import datetime, timedelta

@dag(schedule="@daily", start_date=datetime(2026, 1, 1), catchup=False, tags=["ledger"])
def ledger_consumer():
    wait_upstream = ExternalTaskSensor(
        task_id="wait_upstream",
        external_dag_id="vendor_daily",
        external_task_id="publish_ledger",
        allowed_states=["success"],
        execution_delta=timedelta(hours=1),
        poke_interval=300,
        timeout=14400,
        mode="reschedule",
    )

    @task
    def reconcile():
        return {"status": "reconciled"}

    wait_upstream >> reconcile()

ledger_consumer()
📊 Production Insight
One-hour delta mismatch waited 9 hours.
Date alignment fixed it in minutes.
Rule: verify dates before tuning.
🎯 Key Takeaway
External sensors need exact dates.
Deltas must match schedules.
Assets beat date math.

Write a Custom Sensor

Subclass BaseSensorOperator and implement poke() returning True or False. Return False for not-ready, True for ready, and raise only on auth failures.

Catch transient network errors inside poke and return False so retries handle blips. Keep poke fast under 30 seconds and idempotent across calls.

Modern Airflow also offers @task.sensor, which turns a Python function into a sensor returning PokeReturnValue(is_done=..., xcom_value=...). The xcom_value lands in XCom on success, so the waiter can hand the found partition path straight downstream with zero extra tasks. Keep poke under 30 seconds, catch transient errors and return False, and reserve raises for auth failures.

💡Keep Poke Pure and Fast
No writes inside poke, only reads. A 5-second HEAD request beats a 2-minute download. Deferrable variants free even the poke overhead for long waits.
📊 Production Insight
Pure poke survived 3 vendor blips.
Side-effect poke double-loaded rows.
Rule: poke reads, tasks write.
🎯 Key Takeaway
Poke returns booleans only.
Transients return False.
Fast and pure wins.
● Production incidentPOST-MORTEMseverity: high

The FileSensor That Poked 4,320 Times and Starved the Workers

Symptom
The ledger team's ledger_daily DAG waited on vendor_ledger.csv with a FileSensor poking every 10 seconds. The vendor SFTP job failed silently, so the file never landed. The sensor poked 4,320 times over 12 hours, held one Celery worker slot continuously, and blocked 6 revenue tasks behind it. No alert fired because the sensor was still technically running, and it didn't fail once. Morning revealed a green running sensor and a red SLA.
Assumption
The team assumed default sensor settings were safe for overnight waits. They had used poke mode for short 2-minute waits in dev and it behaved well. Nobody scaled the math to a 12-hour vendor delay or checked worker slot impact. The timeout was left at the default, which meant effectively no deadline.
Root cause
Default poke mode occupies a worker slot for the full wait, and poke_interval of 10 seconds kept the worker busy all night. No timeout was set, so the sensor never failed fast. No soft_fail meant a late file could not skip gracefully. The vendor delay combined with slot starvation to stall the whole worker queue behind a wait that should have been nearly free.
Fix
The sensor was switched to mode="reschedule" with poke_interval=300 and timeout=21600 plus soft_fail=True for non-critical partitions. Long waits moved to deferrable SFTP sensors backed by the triggerer. A vendor SLA alert now fires at 80% of timeout, so silence can't hide a missing file again. Worker slot usage during waits dropped from 100% to near zero.
Key lesson
  • Never use poke mode for waits over 5 minutes; reschedule or defer instead — don't hold a slot all night for one file.
  • Every sensor needs an explicit timeout and a decision on soft_fail.
  • Alert on sensor age, not just sensor failure, to catch silent vendor delays.
Production debug guidePoke storms, silent waits, and slot starvation — with exact commands.4 entries
Symptom · 01
Sensor runs for hours with no downstream progress
Fix
Check sensor age with airflow tasks states-for-dag-run ledger_daily manual__2026-09-01. Inspect logs via airflow tasks logs ledger_daily wait_vendor_file 1. Confirm the file path with ls -lh /data/landing/ on the worker host. Check for reschedule with sub-5-minute pokes overloading the scheduler before blaming the vendor. Check for reschedule with sub-5-minute pokes overloading the scheduler before blaming the vendor.
Symptom · 02
Workers full of sensors, real tasks stuck queued
Fix
Run airflow pools list and airflow jobs check --job-type SchedulerJob. Switch sensors to mode reschedule with poke_interval 300. Audit with rg mode=.poke. dags/ and flip long waits.
Symptom · 03
ExternalTaskSensor never fires though upstream succeeded
Fix
Verify execution dates match with airflow dags list-runs -d upstream_daily. Check allowed_states and execution_delta settings. Test with airflow tasks test ledger_daily wait_upstream manual__2026-09-01.
Symptom · 04
Sensor fails instantly instead of waiting
Fix
Read the poke log for permission errors. Validate connection with airflow connections get vendor_sftp. Fix paths and re-run with airflow dags test ledger_daily 2026-09-01.
★ Sensor Production Debug Cheat SheetFive sensor failures you will hit on call, with commands to run first.
Sensor poking for hours, downstream stuck
Immediate action
Confirm whether the awaited file or partition actually exists
Commands
airflow tasks logs ledger_daily wait_vendor_file 1 --tail 100
ls -lh /data/landing/vendor_ledger.csv && date -u
Fix now
If the file is missing, page the vendor. Set timeout 21600 and poke_interval 300 with mode reschedule so the wait stops costing a slot.
All workers busy, queue full of sensors+
Immediate action
Identify poke-mode sensors hogging slots
Commands
airflow pools list
rg -n "mode=.poke." dags/
Fix now
Flip waits over 5 minutes to mode reschedule or deferrable=True and redeploy. Clear one starved task to confirm flow.
ExternalTaskSensor never succeeds+
Immediate action
Compare logical dates between producer and consumer
Commands
airflow dags list-runs -d upstream_daily --limit 5
airflow tasks test ledger_daily wait_upstream manual__2026-09-01
Fix now
Align execution_delta or switch to asset-triggered scheduling so the consumer fires on the producer outlet.
Sensor times out and fails the DAG on late but valid data+
Immediate action
Decide whether late data should skip or fail
Commands
airflow dags show ledger_daily | grep -A2 wait_vendor
airflow variables get vendor_sla_hours
Fix now
Set soft_fail True for non-critical partitions so timeout marks skipped, and keep fail for money-path partitions.
Custom sensor throws on transient network blips+
Immediate action
Check whether poke raises instead of returning False
Commands
airflow tasks logs ledger_daily wait_api_ready 1 --tail 60
python -c "import socket; print(socket.gettimeout())"
Fix now
Catch transient errors in poke() and return False; reserve exceptions for auth failures. Add retries 3 with retry_delay 60s.
Poke vs Reschedule vs Deferrable Compared
ModeSlot costBest wait
pokeFull wait holds a slotUnder 60 seconds
rescheduleSlot only during pokeMinutes to hours
deferrableNear zero, triggerer handles itHours to days
TimeSensorReschedule recommendedClock-time waits
ExternalTaskSensorReschedule mandatoryCross-DAG waits
exponential_backoffGrowing gaps, capped by max_waitFlaky vendor APIs
soft_fail + branchSkip routes to no-data pathOptional feeds
@task.sensorPokeReturnValue + XCom handoffLightweight custom waits
⚙ Quick Reference
3 commands from this guide
FileCommand / CodePurpose
dagsledger_daily.pyfrom airflow.decorators import dag, taskWhat a Sensor Is
dagsledger_optional.pyfrom airflow.decorators import dag, taskTimeout and Soft Fail
dagsledger_consumer.pyfrom airflow.decorators import dag, taskExternalTaskSensor and Its Exactness Gotcha

Key takeaways

1
Sensors wait for events; timeouts turn silent waits into actionable alerts.
2
Use reschedule or deferrable for any wait over 5 minutes. Sub-5-minute intervals stay on poke; hourly waits go deferrable. Sub-5-minute intervals stay on poke; hourly waits go deferrable.
3
Set timeout and soft_fail deliberately per dataset criticality. The 7-day default is never the right deadline; backoff plus max_wait protects vendors. The 7-day default is never the right deadline; backoff plus max_wait protects vendors.
4
Align ExternalTaskSensor dates exactly or switch to assets.
5
Keep custom poke fast, pure, and tolerant of transients.

Common mistakes to avoid

4 patterns
×

Using poke mode for overnight waits

Symptom
Worker slots fill with sensors and revenue tasks sit queued for hours.
Fix
Use mode reschedule with poke_interval 300 or deferrable sensors for waits over 5 minutes. Rule of thumb: seconds-latency pokes, minute-plus reschedules, hour-plus deferrable; never reschedule faster than ~5 minutes. Rule of thumb: seconds-latency pokes, minute-plus reschedules, hour-plus deferrable; never reschedule faster than ~5 minutes.
×

Leaving timeout unset

Symptom
Sensor runs 12 hours silently and SLAs miss with no alert.
Fix
Set timeout like 21600 and alert on sensor age at 80% of timeout. The default is 7 days; set per-dataset deadlines and remember retries stretch the wall clock. The default is 7 days; set per-dataset deadlines and remember retries stretch the wall clock.
×

Mismatched dates on ExternalTaskSensor

Symptom
Consumer waits forever though the producer succeeded.
Fix
Align execution_delta with schedules or switch to asset-triggered DAGs.
×

Raising inside poke on transient errors

Symptom
Sensor fails on a 5-second network blip instead of retrying.
Fix
Catch transients and return False; raise only on auth or config errors. silent_fail keeps waiting through poke errors; soft_fail converts deadline misses to skips. silent_fail keeps waiting through poke errors; soft_fail converts deadline misses to skips.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between poke and reschedule mode?
Q02SENIOR
How do timeout and soft_fail interact?
Q03SENIOR
When would you use a deferrable sensor over reschedule?
Q01 of 03JUNIOR

What is the difference between poke and reschedule mode?

ANSWER
Poke holds a worker slot for the whole wait; reschedule frees it between checks. Use poke under 60 seconds and reschedule for longer waits.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is an Airflow sensor?
02
When should I use reschedule mode?
03
What does soft_fail do?
04
Why is my ExternalTaskSensor stuck?
05
Are deferrable sensors always better?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.

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 Branching and Trigger Rules
12 / 37 · Airflow
Next
Airflow Conditional Execution