Airflow Sensors: 4,320 Pokes and Still No File in Sight
Airflow sensors wait by poking — every 30 seconds they burn a worker slot.
20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.
- ✓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.
- 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.
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.
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.
- 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.
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.
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.
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.
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.
poke() and the settings.poke() is the whole contract.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).
The 4,320-Poke Night
- 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.
airflow tasks logs feed_ingestion wait_for_feed 2026-07-15airflow tasks states feed_ingestion wait_for_feed 2026-07-15| File | Command / Code | Purpose |
|---|---|---|
| feed_ingestion.py | from airflow.decorators import dag | 1. What a Sensor Is |
| feed_ingestion.py | from airflow.providers.common.filesystem.sensors.file import FileSensor | 3. timeout and soft_fail |
| daily_analytics.py | from airflow.sensors.external_task import ExternalTaskSensor | 5. ExternalTaskSensor and Its Exactness Gotcha |
| new_file_sensor.py | from pathlib import Path | 6. Write a Custom Sensor |
Key takeaways
Common mistakes to avoid
4 patternsUsing default poke mode for long waits
Setting no timeout on a sensor
Sensors watching worker-local file paths
ExternalTaskSensor without execution_delta
Interview Questions on This Topic
What is an Airflow sensor and what does poke_interval control?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.
That's Airflow. Mark it forged?
4 min read · try the examples if you haven't