Skip to main content
scheduler

Scheduler (cron, Airflow, Temporal timers)

Runs jobs at a time or interval — the simplest form of eventual asynchronous computation.

Why it exists

Some work must happen periodically: daily reports, weekly billing runs, hourly cache warmups, monthly cleanup. cron on a single machine works for small scale — but at scale, you need distributed schedulers that survive node failure, guarantee at-least-once (or exactly-once) execution, retry on failure, and give you observability (did the job run? how long? what did it produce?).

How it works

A scheduler reads job definitions (schedule + task) and triggers execution when the time arrives. For distributed schedulers, one node holds the 'is scheduler' lease and is the sole trigger (avoids double-fire). Complex schedulers (Airflow, Prefect) support DAGs — jobs that depend on other jobs. Advanced schedulers (Temporal, Cadence) support durable execution: the workflow's state is checkpointed to a database so it survives worker crashes and can wait days for a subsequent step.

Scaling characteristics

Simple schedulers (cron on one box) handle any single-node workload; the limit is worker fan-out, not the scheduler itself. Distributed schedulers scale by adding workers, keeping the scheduler thin. Airflow can trigger millions of jobs/day; Temporal handles millions of long-running workflows concurrently. Cost: scheduler state (DB) is small; worker pools scale linearly with job load.

When to use it

  • Recurring reports and rollups
  • Nightly ETL jobs
  • Warmup or precomputation tasks (cache warming, feature computation)
  • Delayed messages (send a follow-up email in 24 hours)
  • Timeouts (a workflow that pauses for a user response, gives up after 7 days)
  • Backfills of historical data

When NOT to use it

  • Event-triggered work — use a queue, not a scheduler
  • Sub-second recurrence — cron and most schedulers are minute-granular
  • Ad-hoc one-off scripts — engineer running them manually is fine

Failure modes

  • Double execution: two nodes both think they're the scheduler. Mitigate with distributed lock + heartbeat.
  • Missed execution: scheduler node was down when the trigger time passed. Mitigate with catch-up mode (Airflow) or run-once semantics.
  • Long-running job blocks the next scheduled run — always set a timeout + concurrency limit per job
  • Job depends on data that isn't ready yet — Airflow has sensors; Temporal has waitFor
  • Cron drift on a single machine (system clock issues) causes silent misfires

Alternatives

  • cron on a single box — simplest for tiny scale
  • Kubernetes CronJob — cron with pod isolation and retry; great for containerized workloads
  • Airflow — for DAG-based ETL; heavy but powerful
  • Temporal — for long-running, stateful workflows with retries built in
  • AWS EventBridge / GCP Cloud Scheduler — managed cron for cloud native

Interview questions

  • How do you prevent a scheduled job from running twice when you have 3 scheduler nodes?
  • A daily job takes 26 hours some days and overlaps with the next run. What's your fix?
  • Design a system to send 'follow-up in 24 hours' emails. Where does the scheduling state live?
  • How would you retry a failed job — immediately, with backoff, or from a specific checkpoint?
  • Explain how Airflow handles a DAG where one task fails.
  • What's the difference between an idempotent job and one that just retries safely?
The story of scheduling

1975, AT&T: how Unix taught computers to keep an appointment

In 1975, Brian Kernighan and colleagues at Bell Labs shipped Unix v6 with a program called cron — from the Greek chronos, time. It read a text file describing "at 2AM run this script," and did so. Fifty years later, the same syntax runs in every Docker container, every Kubernetes CronJob, every cloud scheduler. 0 2 * * * means the same thing in 2024 as it did in 1975. Few pieces of software have this longevity.

The problem grew as computing distributed. A single-machine cron is trivial. A distributed scheduler — one that runs jobs across many machines, handles crashes, guarantees no-duplicate-runs, and coordinates across time zones — is very hard. Quartz (2001, Java) tried. Google's internal Borg (2003, publicly described 2015) built massive fleet-scheduling. Kubernetes CronJob (2016) brought K8s-native scheduling. And AWS EventBridge Scheduler (2022) offered serverless triggers at cloud scale.

Modern schedulers face problems Kernighan didn't: what if the machine is down at 2AM (missed fires)? What if two machines both fire? What time zone applies for a user in Tokyo? What if the job takes 6 hours and the next run is due before it finishes? Each of these is a distinct design decision, and every scheduler picks differently.

The core insight: a scheduler is "when to run," not "how to run." It fires an event; something else (a queue, a workflow engine, a K8s pod) does the work. Keep the scheduler simple + reliable, and complexity lives in the actual job. Confuse the two and you get either a fragile scheduler or a bloated job runner. The best modern designs (Temporal schedules, AWS EventBridge Scheduler, K8s CronJob) keep the concern surface small: they say "time is now" and delegate everything else.

Scheduler responsibilities
Compute next-fire time
Cron parser, interval math, calendar rules.
Fire at that time
Wake up + trigger downstream (queue, HTTP, Lambda).
Handle failures
Missed fires. Duplicate prevention. Retry policies.
Coordinate + persist state
Across machine crashes, across regions.
What it's NOT: a workflow engine (long-running steps), a queue (async processing), an orchestrator (task dependencies).

Historical timeline

  1. 1975
    Unix cron v6
    Brian Kernighan et al. at Bell Labs. Reads text file, fires jobs. Syntax survives 50 years.
  2. 1987
    Paul Vixie's Vixie cron
    The cron implementation that shipped in BSD + Linux. Sets the standard for decades.
  3. 2001
    Quartz Scheduler (Java)
    Enterprise Java scheduling library. Cron syntax + calendar rules + persistence. Powers thousands of Java apps.
  4. 2003
    Google Borg (internal)
    Massive-scale cluster + job scheduling. Publicly described in 2015 paper. Ancestor of Kubernetes.
  5. 2010
    Apache Oozie for Hadoop
    Workflow + scheduler for Hadoop. Complex XML config. Batch ETL era.
  6. 2011
    Chronos (Airbnb, later Mesos)
    Distributed cron built on Mesos. Handles cluster-scale scheduling. Fault-tolerant.
  7. 2015
    Apache Airflow
    Airbnb releases Airflow — DAG scheduling for data. Scheduler + orchestrator hybrid.
  8. 2016
    Kubernetes CronJob
    K8s brings cron into container era. Automatic pod creation, retries, ttl.
  9. 2017
    AWS CloudWatch Events Scheduler
    AWS adds rule-based scheduling. Trigger Lambda, ECS, Step Functions on schedule.
  10. 2019
    Cloudflare Cron Triggers
    Cloudflare Workers get cron triggers. Sub-minute serverless scheduling at edge.
  11. 2020
    GCP Cloud Scheduler
    Google's managed cron service. Guaranteed at-least-once delivery.
  12. 2022
    AWS EventBridge Scheduler
    AWS replaces CloudWatch Events Scheduler with EventBridge — millions of scheduled events, one-time or recurring.
  13. 2023
    Temporal Schedules GA
    Temporal adds first-class scheduling on top of durable workflows. Cron-style + interval, with backfill.
  14. 2024
    AI scheduling patterns emerge
    Time-based prompt scheduling. Agent triggers on cron. LLMs consume + emit scheduled events.

Four schedule types — pick your firing pattern

Not every scheduled job wants the same trigger. The four common patterns:

Cron expression

0 2 * * * (every day at 2 AM)

The classic. 5-field: minute, hour, day-of-month, month, day-of-week. Extend to 6 with seconds (Quartz).

Best for: daily/hourly/weekly reports, backups, cleanup.

Fixed interval

every 30s / every 5m / every 2h

Simpler than cron for periodic tasks that don't care about calendar alignment.

Best for: health checks, metric flushes, cache refreshes.

One-time (at)

at 2024-11-15 09:00 UTC

Fire exactly once at a specific timestamp. Unix at command. AWS EventBridge Scheduler one-time.

Best for: delayed jobs, reminders, scheduled emails, release windows.

Event + delay

on user.signup + 24h delay → send tip

Fire N hours after some event. Combines with event-driven system. Temporal timers, SQS delay queues.

Best for: onboarding sequences, follow-ups, delayed retries.

Cron syntax gotcha: day-of-month and day-of-week are OR'd, not AND'd, when both specified. 0 0 15 * MON fires the 15th of any month OR any Monday — not "Monday the 15th." Reason for many missed backups.

Scheduling algorithms — how the wheel actually turns

Once you have N million scheduled jobs, "check every one every second" is unaffordable. The classic algorithms:

Sorted min-heap (priority queue)

Store all jobs by next-fire-time in a min-heap. Peek at head. Sleep until that time. Fire, reschedule, re-insert. O(log n) per operation.

Best for: single-machine schedulers, thousands to millions of jobs. Node.js setTimeout uses this.

Timing wheel (hashed wheel)

Ring of buckets, one per time unit. Insert into bucket for next-fire tick. Advance pointer. Fire everything in current bucket. O(1) insert.

Best for: massive-scale short-timers (network stacks, Kafka clients). Netty, Kafka use this.

DB polling with next_fire_time index

Store jobs in DB. Query "SELECT * WHERE next_fire_time <= NOW()" every N seconds. Fire, update next_fire_time.

Best for: distributed schedulers (Quartz, DB-backed cron). Trades latency for simple horizontal scale.

Delay queue (SQS-style)

Push messages with a visibility delay. Broker keeps them invisible until due. Then poll picks them up.

Best for: fire-and-forget delayed triggers. Millions of one-time schedules.

Time zones + DST + leap seconds: the scheduler's nightmare

Cron looks simple until you have global users. "Fire at 9AM Tokyo time" on a machine set to UTC. DST changes twice a year. Leap seconds happen. Time-zone rules change politically. Each is a well-known production bug.

Time-zone bug

A daily report scheduled at "9AM UTC" hits US-East at 5AM (fine) but hits Tokyo at 18:00 (business already ended). Store schedule with an explicit IANA time zone (Asia/Tokyo).

DST double-fire / skip

Spring forward: 2:00-2:59 doesn't exist. Fall back: 1:00-1:59 happens twice. Cron at 2:30AM misses in spring; fires twice in fall. Use tools that respect DST rules (Quartz Calendar, tz-aware cron).

Leap second confusion

2016: a leap-second broke Cloudflare's Go time API — negative durations. Modern advice: rely on smeared clocks (Google, AWS, Cloudflare all smear leap seconds across ~24h).

Clock skew between coordinator + workers

If coordinator's clock is 30s ahead, jobs fire early on the workers. If behind, late. NTP + PTP + clock quality checks.

The lesson: always store schedules as (cron expression, IANA time zone). Never store "9AM Tokyo" as a UTC offset — the offset itself changes with DST rules. Use battle-tested libraries (tzdata, moment-timezone) that ship with rolling updates.

Cron vs modern managed schedulers — what did we gain?

Unix cron works. But it has real limits in modern environments:

Unix cron

  • + Simple, tested for 50 years
  • + Free, no infra
  • - Single-machine (machine down = missed jobs)
  • - No retry, no observability
  • - No time zones (uses machine local time)
  • - Fire-and-forget (no history)

Managed scheduler (EventBridge, K8s CronJob, Temporal)

  • + Distributed — no single point of failure
  • + Retries + observability + history
  • + Time-zone aware
  • + Backfill for missed runs
  • + Concurrent policy (skip / queue / replace)
  • - Cost + vendor dependency
When cron is still right: single-machine batch jobs on a persistent server. Log rotation. Cleanup scripts. Development environments. Anything where machine downtime = fine to skip.

Product comparison

ProductModelStrengthWeakness
Unix cronOSS50-year proven syntax. Zero infra.Single-machine. No retry. No observability.
Kubernetes CronJobOSSK8s-native. Pod-per-run. Concurrency policy. Time-zone aware.Requires K8s. Container startup latency.
AWS EventBridge SchedulerManagedServerless. Millions of schedules. One-time or recurring. Deep AWS integration.AWS-only. Cost at scale.
GCP Cloud SchedulerManagedGCP-native. HTTP + Pub/Sub + App Engine targets. At-least-once.GCP-only. Fewer targets than EventBridge.
Azure Logic Apps + TimerManagedTimer triggers integrated with visual workflow builder.Azure-only. Cost.
Cloudflare Cron TriggersManagedRuns at 300+ PoPs. Sub-minute triggers for Workers.Cloudflare Workers-only.
Quartz SchedulerOSS (Java)Rich features: calendars, priority, misfire policy. Battle-tested in enterprise Java.Java-only. Requires DB for persistence.
Apache Airflow (scheduler)OSSDAG-aware. Rich UI. Massive community.Scheduler is a bottleneck. Not for high-frequency triggers.
Temporal SchedulesOSS + CloudBuilt on durable workflows. Backfill support. Rich policies.Requires Temporal deployment.
Chronos (Mesos)OSSDistributed cron on Mesos. Fault-tolerant.Mesos-dependent. Declining ecosystem.
Sidekiq Cron / Ent SidekiqOSS + EnterpriseRuby-native. Simple cron addon for Sidekiq.Ruby-only. Redis-dependent.
BullMQ (Node.js)OSSRepeatable jobs + delay queues on Redis. Node.js-native.Node.js-only. Redis-dependent.

How to choose: K8s shop → CronJob. AWS → EventBridge Scheduler. GCP → Cloud Scheduler. Cloudflare Workers → Cron Triggers. Java enterprise → Quartz. Data pipelines → Airflow. Long workflows → Temporal Schedules. Single VM → good old Unix cron.

12 real-world scheduler deployments

GitHub Actions

Millions of scheduled workflows

GitHub Actions supports on: schedule: cron triggers. Millions of workflows fire on schedule across repos. Deduplication + backfill handled by GitHub's scheduler infrastructure.

AWS

EventBridge Scheduler at cloud scale

AWS EventBridge Scheduler runs millions of one-time + recurring schedules. Powers many AWS services (Lambda, ECS, Step Functions triggers). At-least-once delivery + FIFO ordering.

Kubernetes

CronJob for cluster maintenance

Every K8s cluster runs CronJobs for cleanup, backups, cert renewal, log rotation. Time zones added in K8s 1.24. Concurrency policy prevents overlapping runs.

Airbnb

Airflow: thousands of DAGs daily

Airbnb runs thousands of Airflow DAGs per day — hourly, daily, weekly. Data warehousing, ML training, marketing analytics. All driven by Airflow's scheduler.

Netflix

Meson (retired) + custom

Netflix built Meson — internal scheduler for ML pipelines. Retired in favor of Argo Workflows + Metaflow. Complex data + ML orchestration.

Uber

Cadence timers at massive scale

Uber uses Cadence timers for millions of scheduled events — driver reminders, expiring offers, follow-up tasks. Durable across crashes.

Cloudflare

Cron Triggers running at edge

Cloudflare Workers can be triggered by cron across 300+ edge PoPs. Serverless, no infra. Cheap for many small periodic tasks.

Shopify

Sidekiq Cron + Enterprise

Shopify runs Sidekiq Cron Enterprise for periodic Ruby jobs — merchant analytics, inventory sync, notification batches. Powers vast fleet of background jobs.

OpenAI

Scheduled fine-tuning + batch API

OpenAI schedules fine-tuning jobs + batch API processing on internal orchestration. Runs continuously with retry + backoff.

Discord

GCP Cloud Scheduler for cleanup + reports

Discord uses GCP Cloud Scheduler for periodic tasks — TTL cleanup, weekly reports, cache warming. Simple, reliable, GCP-integrated.

Stripe

Custom scheduler for payouts

Stripe runs custom scheduling infrastructure for monthly payouts, subscription renewals, invoice generation. Financial correctness demands exactly-once semantics.

Linear

Temporal Schedules for reminders

Linear (project management) uses Temporal Schedules to fire user reminders, sync integrations, run periodic queries. Durable + observable.

Key takeaways

  • 1A scheduler is "when to run," not "how to run." Fire an event and delegate the work.
  • 2Cron syntax is 50 years old and universal. Learn it. But use tools that respect time zones + DST.
  • 3Distributed schedulers need to handle missed fires (machine down), duplicates (multiple firers), and concurrency.
  • 4Common algorithms: min-heap (single-machine), timing wheel (Kafka-scale), DB polling (distributed), delay queue (fire-and-forget).
  • 5Store schedules with IANA time zones, not UTC offsets. DST + political changes break offset-based storage.
  • 6For most modern apps: K8s CronJob, AWS EventBridge Scheduler, GCP Cloud Scheduler, or Temporal Schedules. Rarely raw cron.
  • 7Concurrency policy matters: what if the last run isn't done? Skip, queue, or replace? Every scheduler exposes this.
  • 8For long-running jobs, pair a lightweight scheduler with a workflow engine (Temporal, Step Functions). Scheduler fires, workflow does the work.

References & further reading

  • Kernighan, B. (1975). Unix cron original documentation. Historical.
  • Vixie, P. (1987). Vixie cron implementation notes. The version that shipped everywhere.
  • Verma, A. et al. (2015). "Large-scale cluster management at Google with Borg." EuroSys. Google's internal scheduler.
  • Varghese, G. & Lauck, T. (1987). "Hashed and Hierarchical Timing Wheels." SOSP. Foundational timing-wheel paper.
  • Quartz Scheduler Documentation: Comprehensive scheduler concepts (misfire policy, calendars, priorities).
  • Kubernetes Docs: "CronJob" — concurrency policy, TTL, best practices.
  • AWS Docs: "Amazon EventBridge Scheduler." The modern managed scheduler paradigm.
  • Airflow Docs: "Scheduler" section. How DAG-aware scheduling works.
  • Temporal Docs: "Schedules." Modern durable scheduling on top of workflows.
  • IANA Time Zone Database: The source of truth for time-zone rules. Ships in every OS.