# System Design Interview: Design a Task Scheduling System (Cron/Airflow)

Source: https://www.techinterview.org/post/3233463176/system-design-task-scheduling-system/
Updated: 2026-04-17 · techinterview.org

## What Is a Task Scheduling System?

A task scheduling system triggers jobs at specified times or on recurring schedules. Examples: Unix cron, Apache Airflow, AWS EventBridge Scheduler, Celery Beat. Core challenges: exactly-once execution (no job fires twice), reliability under scheduler crashes, distributed clock synchronization, and handling long-running vs short tasks differently.

- [Shopify Interview Guide](https://techinterview.org/shopify-software-engineer-interview/)

- [LinkedIn Interview Guide](https://techinterview.org/linkedin-software-engineer-interview/)

- [Databricks Interview Guide](https://techinterview.org/databricks-interview-guide/)

- [Stripe Interview Guide](https://techinterview.org/stripe-software-engineer-interview/)

- [Airbnb Interview Guide](https://techinterview.org/airbnb-software-engineer-interview/)

- [Uber Interview Guide](https://techinterview.org/uber-software-engineer-interview/)

## System Requirements

### Functional

  - Schedule jobs: one-time (fire at T) and recurring (cron expression)

  - Execute jobs reliably: at-least-once, with idempotency for exactly-once

  - Monitor: job history, success/failure, execution duration

  - Priority queues: urgent jobs run before lower-priority ones

  - Dependencies: Job B starts only after Job A completes (DAG)

### Non-Functional

  - 100M scheduled jobs, 10K executions/second

  - Job trigger latency: <1 second from scheduled time

  - 99.99% reliability (no missed jobs)

## Core Data Model


```
jobs: id, name, schedule(cron/timestamp), handler, payload,
      status(active/paused/deleted), max_retries, timeout_seconds
executions: id, job_id, status(pending/running/success/failed),
            started_at, completed_at, worker_id, attempt_number
```


## Scheduling Architecture


```
Scheduler Service ──polls DB──► jobs due within next 30s
        │
   Acquires distributed lock (per job)
        │
   Creates execution record + publishes to Job Queue (Kafka/SQS)
        │
Worker Pool ──consumes──► executes handler ──► updates execution status
```


## Polling vs Event-Driven Trigger

Polling: the scheduler queries for jobs due in the next N seconds, runs every N/2 seconds. Simple, works for up to millions of jobs. At 100M jobs: partition the jobs table by next_run_time bucket; each scheduler instance owns a range. Event-driven (time-wheel or delay queue): store jobs in a sorted set (Redis ZADD with score = next_run_unix_timestamp). Poll Redis ZRANGEBYSCORE min max with max = now + 5s. Lower latency, handles high volumes efficiently.


```
# Redis time-wheel approach
ZADD scheduled_jobs {next_run_unix} job_id
# Scheduler loop (every 1 second):
due = redis.zrangebyscore('scheduled_jobs', 0, time.time() + 1)
for job_id in due:
    redis.zrem('scheduled_jobs', job_id)
    enqueue(job_id)
    if recurring: redis.zadd('scheduled_jobs', next_time(job), job_id)
```


## Exactly-Once Execution

The scheduler may crash after enqueuing a job but before recording it as enqueued — causing double execution on restart. Solution: use a distributed lock per job (Redis SET NX EX) before enqueuing. Only the scheduler holding the lock enqueues the job. Lock TTL = job execution timeout + buffer. If the worker crashes mid-execution: the execution record stays in "running" state. A watchdog process detects executions running longer than timeout_seconds and marks them as failed, releasing the lock for retry.

## Cron Expression Parsing


```
* * * * *    every minute
0 9 * * MON  every Monday at 9am
0 */4 * * *  every 4 hours
```

Parse cron expressions to compute next_run_time. Use a library (croniter in Python, cron-parser in Node). Store next_run_time in the DB; after execution, compute and update the next occurrence.

## DAG Dependencies (Airflow-style)

For pipeline orchestration: model tasks as a DAG. Store edges in a task_dependencies table. Before executing a task, check all upstream tasks in the current run have status=success. A dependency resolver runs after each task completion and enqueues newly unblocked tasks.

## Retry and Backoff

On task failure: increment attempt_number, compute next_retry_at = now + 2^attempt * base_delay (exponential backoff with jitter). Cap at max_retries. After max_retries: mark as permanently failed, send alert. Idempotent handlers make retries safe.

## Interview Tips

  - Redis sorted set (score = next_run_unix) is the canonical time-wheel data structure.

  - Distributed lock per job prevents double-execution at the scheduler level.

  - Separate scheduler (which decides when) from worker (which executes) for independent scaling.

  - DAG dependency model is Airflow's core — mention it for pipeline use cases.
