Fixed Intervals vs Clock Times
Concepts covered: Data intervals
schedule=timedelta(hours=6) builds a DeltaDataIntervalTimetable. Its first window opens at start_date to the second, and every window after that opens where the previous one closed, 6 hours later, with no reference to the clock on the wall. schedule=CronDataIntervalTimetable("0 */6 * * *", timezone="UTC") opens windows at 00:00, 06:00, 12:00 and 18:00 UTC. With a start_date of 1 November 2026 at 01:30 UTC, the interval DAG's first window is 01:30 to 07:30 and its first run appears at 07:30. The cron DAG's first window opens at the first aligned point at or after start_date, 06:00, and its first run appears at 12:00. The 4 and a half hours between 01:30 and 06:00 belong to no window of the cron DAG, ever.
| window | timedelta(hours=6) | run appears | 0 */6 * * * | run appears |
|---|---|---|---|---|
| 1 | 1 Nov 01:30 to 07:30 | 1 Nov 07:30 | 1 Nov 06:00 to 12:00 | 1 Nov 12:00 |
| 2 | 1 Nov 07:30 to 13:30 | 1 Nov 13:30 | 1 Nov 12:00 to 18:00 | 1 Nov 18:00 |
| 3 | 1 Nov 13:30 to 19:30 | 1 Nov 19:30 | 1 Nov 18:00 to 2 Nov 00:00 | 2 Nov 00:00 |
| 4 | 1 Nov 19:30 to 2 Nov 01:30 | 2 Nov 01:30 | 2 Nov 00:00 to 06:00 | 2 Nov 06:00 |
The interval DAG carries start_date's minute forever: change start_date to 01:45 and every window moves by 15 minutes, and every logical date with it. The cron DAG ignores the minute and aligns to the clock, so the same change moves nothing. Under a clock change the 2 part ways: a 6-hour timedelta stays 6 hours through the night of 29 March, while 0 */6 * * * in Europe/Berlin fires at 00:00, 06:00, 12:00 and 18:00 local, so the window from midnight to 06:00 local that night holds 5 hours of UTC time and the matching window in October holds 7.
The step that wraps at midnight
0 */5 * * * reads as every hour divisible by 5, from 00 through 20. After 20:00 the next match is 00:00, 4 hours on, because the hour field restarts at 0 each day. timedelta(hours=5) keeps a 5-hour step and walks through the clock: 00:00, 05:00, 10:00, 15:00, 20:00, 01:00, 06:00. Over a week the cron fires 35 times at the same 5 clock times each day; the interval closes 33 windows and visits every hour of the day. Every 6 hours and 0 */6 * * * agree only because 24 divides by 6. Every 5 hours and 0 */5 * * * are 2 different schedules, and only one of them is the one the owner meant.
| step | 0 */5 * * * fires at | gap | timedelta(hours=5) window closes at | gap |
|---|---|---|---|---|
| 1 | 1 Nov 05:00 | 5 h | 1 Nov 05:00 | 5 h |
| 2 | 1 Nov 10:00 | 5 h | 1 Nov 10:00 | 5 h |
| 3 | 1 Nov 15:00 | 5 h | 1 Nov 15:00 | 5 h |
| 4 | 1 Nov 20:00 | 5 h | 1 Nov 20:00 | 5 h |
| 5 | 2 Nov 00:00 | 4 h | 2 Nov 01:00 | 5 h |
| 6 | 2 Nov 05:00 | 5 h | 2 Nov 06:00 | 5 h |
Presets are crons
The presets @hourly and @daily expand to cron strings, as does @weekly, so they align to the clock and follow section 1's rule about which reading a bare string gets. timedelta(days=1) is a different schedule from @daily: it opens its first window at start_date rather than at the next midnight, keeps 24 hours through a clock change, and if start_date is 1 November at 09:15 UTC, every run of it is named for 09:15. When a reviewer sees a timedelta where a cron would do, the question to ask is whether the odd minute in the logical dates is intended.
- The data has a clock: business days, partner drops at 06:00 local, hourly partitions. Use a cron in the right zone.
- The work has a cadence: poll every 15 minutes, refresh every 6 hours, whatever the hour. Use a timedelta.
- The cadence divides 24 and DST is irrelevant: either works, and the cron reads better in the grid.
- The cadence does not divide 24: a timedelta, or accept the short step at midnight on purpose.
import pendulum
from datetime import timedelta
from airflow.sdk import dag, task
START = pendulum.datetime(
2026, 11, 1, 1, 30, tz="UTC")
# opens at start_date, not 06:00
SIX = timedelta(
hours=6)
@dag(schedule=SIX,
start_date=START,
catchup=True)
def cache_refresh():
@task
def refresh():
pass
refresh()
cache_refresh()
- 6-hour timedelta
- 6-hourly cron
About this lesson
This section is part of the Scheduling: Intermediate lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.