Home DevOps Airflow Sensors: 4,320 Pokes and Still No File in Sight
Intermediate 4 min · August 1, 2026

Airflow Sensors: 4,320 Pokes and Still No File in Sight

Airflow sensors wait by poking — every 30 seconds they burn 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
August 01, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 15-20 min read
  • Airflow running with at least one scheduled DAG.
  • A wait your pipeline depends on: a file, an object, a task in another DAG.
  • Comfort reading the grid view and task logs.
  • Basic Python and DAG authoring experience.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • A sensor is an operator whose entire task is waiting — poking a condition until it's true or a timeout fires.
  • Poke mode holds a full worker slot for the whole wait; reschedule mode releases it between pokes so other tasks can run.
  • 4,320 pokes at 10-second intervals is 12 hours of slot rental for a file that never arrived — and one starving queue behind it.
  • timeout bounds the wait and soft_fail turns a timeout into a skip, so optional data doesn't sink the DAG.
  • Production rule: mode="reschedule", a real timeout, and deferrable sensors for waits over a few minutes — the triggerer waits for ~free.
  • The default timeout is seven days and the default poke_interval is 60 seconds — set both deliberately.
✦ Definition~90s read
What is Airflow Sensors?

An Airflow sensor is an operator whose entire task is waiting — it pokes a condition on a schedule until the condition is true or a timeout fires, with poke and reschedule modes determining where the wait happens.

Imagine a delivery driver paid by the hour, parked outside a warehouse, checking the gate every 10 seconds for a truck that might never come.
Plain-English First

Imagine a delivery driver paid by the hour, parked outside a warehouse, checking the gate every 10 seconds for a truck that might never come. That's a poke-mode sensor. For 12 hours the driver held the loading dock — 4,320 checks — and no truck showed up. Reschedule mode is the same driver going home between checks, freeing the dock for real work. Deferrable sensors are the driver who leaves a phone number and only gets called when the truck actually arrives.

A partner file lands between 1 AM and 7 AM. The pipeline needs it, so someone wrote a sensor to wait. Twelve hours later, that sensor had poked 4,320 times — once every 10 seconds — and the file still wasn't there. Worse, the wait had rented a full worker slot the entire time, and everything behind it queued.

Sensors are operators whose job is waiting. That's easy to write and easy to get wrong: the defaults that feel harmless — poke mode, no timeout, a tight poke interval — are exactly the settings that burn a slot for 12 hours and fail a DAG at dawn.

This article breaks down what sensors are, the worker-slot economics of poke vs reschedule, timeout and soft_fail semantics, the common sensor catalog, and the ExternalTaskSensor exactness trap. You'll also get the diagnosis flow for a sensor that waits too long.

If your sensor has been running for more than an hour, it's not waiting for the file. It's renting a worker slot to look at the clock.

1. What a Sensor Is: An Operator Whose Work Is Waiting

A sensor is an operator whose whole task is the wait: poke the condition, sleep the interval, poke again — until the condition is true or the timeout fires. Success means the condition became true; timeout means it didn't.

The condition can be a file on disk, an object in S3, a row in a database, a task in another DAG, or the clock itself. Each has a purpose-built sensor, and you can subclass BaseSensorOperator for anything else.

Design the wait like any other task: bounded, monitored, and cheap. The 4,320-poke night was none of those.

feed_ingestion.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from airflow.decorators import dag
from airflow.providers.common.filesystem.sensors.file import FileSensor
from pendulum import datetime


@dag(schedule="@daily", start_date=datetime(2026, 8, 1), catchup=False, tags=["feeds"])
def feed_ingestion():
    wait_for_feed = FileSensor(
        task_id="wait_for_feed",
        filepath="/data/feeds/partner_orders.csv",
        poke_interval=60,
        timeout=3600,
        mode="reschedule",
    )


feed_ingestion()
Output
2026-07-15T01:12:00 INFO - Poking: /data/feeds/partner_orders.csv ...
🔥The Wait Is the Work
A sensor doesn't do a small check now and then. Its entire task is the wait — the poke, the pause, the next poke. Design the wait like you'd design any other task: bounded, monitored, and cheap.
📊 Production Insight
A sensor's deliverable is the wait itself.
Bounded, monitored, cheap — every sensor needs all three.
Poke, sleep, repeat is the whole job description.
🎯 Key Takeaway
A sensor is an operator whose work is waiting.
Success means the condition became true in time.
Design the wait; don't just inherit the defaults.
airflow-sensors-diagram1 A Sensor's Work Is the Wait Poke, sleep, repeat — the whole job Poke the condition file, S3 object, DB row, other task Condition true? sleep the poke interval between yes Success — wait over the condition became true no Sleep the poke interval then poke again Timeout fires the wait ends Design the wait bounded · monitored · cheap THECODEFORGE.IO
thecodeforge.io
Airflow Sensors

2. poke vs reschedule: Worker Slot Economics

The mode decides where the wait happens. In poke mode — the default — the task stays in running state on a worker for the entire wait, sleeping between pokes. One sensor, one slot, for hours.

In reschedule mode, the task releases the slot between pokes and re-enters the queue. The poke still happens on schedule, but the wait no longer occupies a worker. Your parallelism budget stays yours.

The numbers from the incident: 10-second pokes for 12 hours is 4,320 pokes and one slot held for a full night. At 4 workers, that's a quarter of the fleet parked in front of a gate.

Defaults worth knowing: poke_interval defaults to 60 seconds, and the docs' rule of thumb is poke mode for sub-minute checks, reschedule for minute-level ones — the trade-off is latency, because every reschedule has to re-enter the queue.

Mental Model
A Sensor Is a Slot Renter
In poke mode a sensor rents a full worker slot for its entire wait; in reschedule mode it returns the slot between pokes.
  • Poke mode: one slot held for hours while other tasks queue behind the wait.
  • Reschedule mode: the task releases the slot and re-enters the queue between pokes.
  • The 4,320-poke incident was a 12-hour slot rental for a file that never came.
📊 Production Insight
Poke mode is a slot rental with a long lease.
Reschedule mode returns the slot between pokes.
Four sensors in poke mode can park a whole fleet.
🎯 Key Takeaway
Poke mode holds a worker for the entire wait.
Reschedule mode makes the wait nearly free.
The mode is the economics; the interval is the cadence.
airflow-sensors-diagram2 poke vs reschedule: Slot Economics Mode decides where the wait happens poke mode — default reschedule mode Worker slot held for the entire wait released between pokes Grid state running for hours up_for_reschedule Queue impact everything behind queues parallelism stays yours 4,320 pokes, one slot, 12 hours 10s interval all night Production rule: mode=reschedule for waits measured in hours THECODEFORGE.IO
thecodeforge.io
Airflow Sensors

3. timeout and soft_fail: Fail vs Skip vs Keep Waiting

timeout bounds the wait in seconds. Without it, a sensor pokes forever — the DAG never fails, the slot never frees, and the grid view shows a task that's been running for a week. Set timeout to the longest wait you can justify for that data.

soft_fail changes what a timeout means: the task is skipped instead of failed. That's the right call when the data is optional — a marketing feed you can live without — and the wrong call when the data is the reason the DAG exists.

Three outcomes, three designs: success when the file arrived, failed when it didn't and the data is mandatory, skipped when it didn't and the pipeline can proceed.

Two numbers frame the budget: the default timeout is seven days, and timeout counts from the first execution attempt, not per poke — so a rescheduled sensor's wall-clock lifetime can exceed it. When the bound does fire, the task raises AirflowSensorTimeout, which is exactly what soft_fail converts into a skip.

feed_ingestion.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from airflow.providers.common.filesystem.sensors.file import FileSensor

wait_for_feed = FileSensor(
    task_id="wait_for_feed",
    filepath="/data/feeds/partner_orders.csv",
    poke_interval=60,
    timeout=21600,   # 6 hours — the maximum sane wait
    mode="reschedule",
)

# Optional data? Timeout becomes a skip, not a failure.
wait_for_marketing_feed = FileSensor(
    task_id="wait_for_marketing_feed",
    filepath="/data/feeds/marketing_emails.csv",
    poke_interval=120,
    timeout=3600,
    soft_fail=True,
)
Output
wait_for_feed: success (file appeared at 03:40) | wait_for_marketing_feed: skipped (soft_fail after 60 min)
📊 Production Insight
No timeout means a sensor that never concludes.
soft_fail turns a timeout into a skip for optional data.
Timeout is a budget; spend it on data you actually need.
🎯 Key Takeaway
timeout bounds the wait; soft_fail grades the result.
Mandatory data fails; optional data skips.
A sensor without a timeout is a liability with a heartbeat.
airflow-sensors-diagram3 timeout / soft_fail: Grading the Wait Success, fail, or skip at timeout timeout bounds the wait the longest wait you can justify No timeout → pokes forever slot never frees, DAG never fails Wait ends one of three ways: Success file arrived in time Failed mandatory data missing Skipped soft_fail → skip soft_fail: timeout becomes a skip the call for optional data THECODEFORGE.IO
thecodeforge.io
Airflow Sensors

4. The Common Sensor Catalog

You rarely write a sensor from scratch — the catalog covers the waits you'll actually meet. FileSensor watches a filesystem path, S3KeySensor watches for an object, SqlSensor polls until a query returns rows, TimeSensor and TimeDeltaSensor wait on the clock.

The one that confuses people is ExternalTaskSensor, which waits for a task instance in another DAG — it has its own section below because its exactness trap has cost teams a night or two.

When nothing fits, subclass BaseSensorOperator and implement poke(): return True when the wait is over. The table below is the catalog with the notes that matter.

The TaskFlow API has a shortcut too: the @task.sensor decorator turns any function that returns a PokeReturnValue into a sensor, which covers many quick waits without writing a class.

📊 Production Insight
The catalog covers almost every wait you'll meet.
FileSensor and S3KeySensor are the daily drivers.
Custom sensors are the exception, not the norm.
🎯 Key Takeaway
Purpose-built sensors cover the common waits.
The catalog's gotchas live in the details.
Write a custom poke() only when the catalog genuinely can't.

5. ExternalTaskSensor and Its Exactness Gotcha

ExternalTaskSensor waits for a task instance in another DAG to reach success. The gotcha: by default it matches the same data interval, not the same wall clock. If the upstream DAG runs on a different schedule or lands late, the sensor waits for a run that isn't the one you mean.

execution_delta shifts the match by a fixed amount — upstream schedules two hours later, offset by two hours. execution_date_fn handles anything irregular.

The failure shape is familiar: upstream succeeded hours ago, the sensor pokes forever, and the grid shows two healthy DAGs with one invisible mismatch between them.

daily_analytics.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
from airflow.sensors.external_task import ExternalTaskSensor
from datetime import timedelta

wait_for_orders = ExternalTaskSensor(
    task_id="wait_for_orders_dag",
    external_dag_id="nightly_orders",
    external_task_id="load_orders",
    execution_delta=timedelta(hours=2),  # match the upstream's later schedule
    timeout=3600,
    mode="reschedule",
)
Output
matched nightly_orders.load_orders for data_interval 2026-07-14 22:00 — success
📊 Production Insight
ExternalTaskSensor matches data intervals, not clocks.
Different schedules need execution_delta to align.
Two green DAGs can hide one mismatched interval.
🎯 Key Takeaway
The exactness gotcha is the interval, not the DAG.
Align schedules with execution_delta or execution_date_fn.
If it never fires, compare the intervals first.

6. Write a Custom Sensor

Subclass BaseSensorOperator and implement poke(context): return True when the wait is over, False to poke again. Every BaseSensorOperator setting — poke_interval, timeout, mode, soft_fail — works unchanged.

Keep the poke cheap: it runs on every interval, so a poke that lists a directory is fine; a poke that scans a terabyte is a problem you've built yourself.

For the partner-file wait, the catalog's FileSensor already fits. Custom sensors earn their keep for domain conditions — a row reaching a state, an API returning a flag — that no provider sensor covers.

Two more knobs for long waits: exponential_backoff=True widens the gap between pokes on each failure, bounded by max_wait — the polite way to poll an external system with unpredictable availability.

new_file_sensor.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pathlib import Path

from airflow.sensors.base import BaseSensorOperator


class NewFileSensor(BaseSensorOperator):
    def __init__(self, filepath: str, **kwargs):
        super().__init__(**kwargs)
        self.filepath = filepath

    def poke(self, context):
        return Path(self.filepath).exists()


wait_for_partner_file = NewFileSensor(
    task_id="wait_for_partner_file",
    filepath="/data/feeds/partner_orders.csv",
    poke_interval=60,
    timeout=3600,
    mode="reschedule",
)
Output
2026-07-15T03:41:02 INFO - Poking returned True — sensor succeeded
📊 Production Insight
A custom sensor is a poke() and the settings.
Keep the poke cheap; it runs on every interval.
Reach for the catalog first, subclass second.
🎯 Key Takeaway
BaseSensorOperator + poke() is the whole contract.
A cheap poke is a production requirement.
Inherit the settings, write only the condition.

7. Reading the Grid When a Sensor Waits

A sensor in running state for 12 hours looks like a stuck task. The logs tell the truth: a Poking line every interval means the wait is alive and renting. Check three numbers — poke_interval, timeout, mode — before you suspect the scheduler.

The states help: up_for_reschedule in the grid means the sensor released its slot and is waiting cheaply. Running for hours with poke mode means the slot is held hostage.

And when the wait genuinely needs to be long — hours, not minutes — skip the slot question entirely: deferrable sensors hand the wait to the triggerer process, where it costs almost nothing (that's the deferrable-operators article, and it's the right upgrade path for every sensor in this one).

📊 Production Insight
Poking logs prove the wait is alive, not stuck.
up_for_reschedule is the healthy waiting state.
Long waits belong in the triggerer, not on a worker.
🎯 Key Takeaway
The grid view grades the wait if you read it right.
Running + poke mode = slot rental; up_for_reschedule = cheap.
For hour-long waits, deferrable sensors are the endgame.
● Production incidentPOST-MORTEMseverity: high

The 4,320-Poke Night

Symptom
The feed_ingestion DAG showed a sensor in running state for 12 straight hours. Logs showed a Poking line every 10 seconds — 4,320 pokes — while the rest of the pipeline queued behind the occupied slot.
Assumption
The team assumed a sensor was lightweight — a background watcher that barely costs anything — and that a long wait just meant the pipeline would catch the file whenever it landed.
Root cause
Default poke mode keeps the task in running state on a worker for the entire wait. Poking every 10 seconds held a full worker slot for 12 hours, so every task behind it queued. No timeout was set, so the sensor would have poked until the file appeared — or forever.
Fix
Switched to mode="reschedule" so the task releases its slot between pokes, set timeout to 6 hours and poke_interval to 60 seconds, and moved the wait into a deferrable sensor so the triggerer does the waiting instead of a worker.
Key lesson
  • Poke mode rents a full worker slot for the entire wait — a 12-hour sensor is a 12-hour slot outage.
  • 4,320 pokes at 10-second intervals is an idle-slot rental nobody notices until the queue backs up.
  • timeout bounds the wait; without it a sensor pokes forever and the DAG never fails.
  • Reschedule mode and deferrable sensors move the wait out of workers — that's the economics that matter.
Production debug guideSymptom to Action5 entries
Symptom · 01
A sensor has been running for hours
Fix
Check poke_interval, timeout, and mode in the rendered params; logs full of Poking lines confirm the wait is still active.
Symptom · 02
A sensor timed out and failed the DAG
Fix
Decide whether the data is optional — soft_fail=True skips instead of failing — or whether the timeout is too tight for the real arrival window.
Symptom · 03
Tasks queue behind a sensor
Fix
Poke mode holds the slot for the whole wait; switch to mode="reschedule" or a deferrable sensor to free it between pokes.
Symptom · 04
ExternalTaskSensor never fires though the upstream succeeded
Fix
Verify external_dag_id and external_task_id, then compare data intervals — a schedule mismatch means it watches the wrong run.
Symptom · 05
A sensor misses a file that exists
Fix
Check the filepath semantics: sensors run on the worker, so local paths differ across workers — use shared storage or an object-storage sensor.
★ Sensor Triage CommandsFirst-response commands for sensors that wait too long.
Sensor shows running for hours
Immediate action
Count the pokes in the logs
Commands
airflow tasks logs feed_ingestion wait_for_feed 2026-07-15
airflow tasks states feed_ingestion wait_for_feed 2026-07-15
Fix now
Switch to mode=reschedule and set a real timeout.
Sensor finally timed out after a full shift+
Immediate action
Check timeout vs poke_interval
Commands
airflow tasks render feed_ingestion wait_for_feed 2026-07-15
airflow dags list-runs feed_ingestion
Fix now
Set timeout explicitly; the default wait is a slot rental you never budgeted.
Tasks queue behind a sensor+
Immediate action
Check running tasks and slot usage
Commands
airflow tasks states feed_ingestion wait_for_feed 2026-07-15
airflow config get-value core parallelism
Fix now
Move the wait out of workers: reschedule mode or deferrable=True.
ExternalTaskSensor never fires though upstream succeeded+
Immediate action
Compare data intervals across DAGs
Commands
airflow dags list-runs nightly_orders
airflow tasks states nightly_orders load_orders 2026-07-15
Fix now
Set execution_delta or execution_date_fn to match the upstream run.
Sensor failed the DAG when the file was just late+
Immediate action
Decide fail vs skip for optional data
Commands
airflow tasks render feed_ingestion wait_for_feed 2026-07-15
airflow tasks clear feed_ingestion --task-regex wait_for_feed
Fix now
soft_fail=True skips instead of failing when the data is optional.
The Common Sensor Catalog
SensorWaits forNotes
FileSensorA file on a filesystem pathPath is worker-local — use shared storage or object storage
S3KeySensorAn object in S3Native object-storage wait; pass bucket_key and bucket_name
ExternalTaskSensorA task instance in another DAGMatches data intervals — needs execution_delta when schedules differ
SqlSensorA query that returns a rowPoll a database condition; keep the query small
TimeSensor / TimeDeltaSensorA wall-clock time / elapsed durationCheap waits with no external state involved
Custom BaseSensorOperatorYour own conditionpoke() returns True when the wait is over
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
feed_ingestion.pyfrom airflow.decorators import dag1. What a Sensor Is
feed_ingestion.pyfrom airflow.providers.common.filesystem.sensors.file import FileSensor3. timeout and soft_fail
daily_analytics.pyfrom airflow.sensors.external_task import ExternalTaskSensor5. ExternalTaskSensor and Its Exactness Gotcha
new_file_sensor.pyfrom pathlib import Path6. Write a Custom Sensor

Key takeaways

1
A sensor is an operator whose work is the wait
the poke, the pause, the next poke.
2
Poke mode rents a worker slot for the whole wait; reschedule mode releases it between pokes.
3
timeout bounds the wait; without it a sensor never fails, it just waits forever.
4
ExternalTaskSensor matches data intervals, not wall clocks
align it with execution_delta.
5
Prefer deferrable sensors for waits over a few minutes; the triggerer waits for almost nothing.

Common mistakes to avoid

4 patterns
×

Using default poke mode for long waits

Symptom
One sensor holds a full worker slot for hours while everything behind it queues
Fix
Set mode="reschedule" so the task releases its slot between pokes, or use deferrable=True.
×

Setting no timeout on a sensor

Symptom
The sensor pokes forever, the DAG never fails, and the slot never frees
Fix
Set timeout to the longest wait you can justify for that data — the seven-day default is not a plan.
×

Sensors watching worker-local file paths

Symptom
The file exists on one worker but the sensor on another never finds it
Fix
Use shared storage or an object-storage sensor like S3KeySensor with the file's real location.
×

ExternalTaskSensor without execution_delta

Symptom
The sensor waits forever for a run that completed hours ago on a different schedule
Fix
Set execution_delta or execution_date_fn to align data intervals with the upstream DAG.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is an Airflow sensor and what does poke_interval control?
Q02SENIOR
Your FileSensor ran for 12 hours and starved the queue. What happened an...
Q03SENIOR
Design a wait for a partner file that arrives between 1 AM and 7 AM with...
Q01 of 03JUNIOR

What is an Airflow sensor and what does poke_interval control?

ANSWER
A sensor is an operator whose job is waiting: it pokes a condition until it's true or a timeout fires. poke_interval is the seconds between pokes — the cadence of the checks. The full wait is bounded by timeout, and mode decides whether the wait holds a worker slot (poke) or releases it between checks (reschedule).
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What's the difference between poke mode and reschedule mode?
02
Why does my sensor hold a worker slot for hours?
03
What happens when a sensor times out?
04
How does ExternalTaskSensor match the upstream run?
05
When should I use a deferrable sensor?
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
August 01, 2026
last updated
1,750
articles · all by Naren
🔥

That's Airflow. Mark it forged?

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

Previous
Airflow Branching and Trigger Rules
12 / 37 · Airflow
Next
Airflow Conditional Execution