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.

windowtimedelta(hours=6)run appears0 */6 * * *run appears
11 Nov 01:30 to 07:301 Nov 07:301 Nov 06:00 to 12:001 Nov 12:00
21 Nov 07:30 to 13:301 Nov 13:301 Nov 12:00 to 18:001 Nov 18:00
31 Nov 13:30 to 19:301 Nov 19:301 Nov 18:00 to 2 Nov 00:002 Nov 00:00
41 Nov 19:30 to 2 Nov 01:302 Nov 01:302 Nov 00:00 to 06:002 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.

step0 */5 * * * fires atgaptimedelta(hours=5) window closes atgap
11 Nov 05:005 h1 Nov 05:005 h
21 Nov 10:005 h1 Nov 10:005 h
31 Nov 15:005 h1 Nov 15:005 h
41 Nov 20:005 h1 Nov 20:005 h
52 Nov 00:004 h2 Nov 01:005 h
62 Nov 05:005 h2 Nov 06:005 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.

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
Excerpt, 1 Nov 00:00 to 20:00: the 6-hour interval opens at 01:30, while the cron waits for 06:00 and never covers the gap.

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.