01

Requirements & scope

Problem Statement & Requirements

Design a distributed task scheduling system that reliably enqueues, schedules, and executes tasks (jobs) across a fleet of workers at massive scale — similar to Celery, Temporal, Apache Airflow, Sidekiq, or cloud-native solutions like AWS Step Functions and Google Cloud Tasks.

Functional Requirements

  • One-time tasks — submit a task for immediate execution (e.g., "send this email now")
  • Delayed tasks — schedule a task for a specific future time (e.g., "send reminder in 24 hours")
  • Recurring / Cron tasks — periodic execution on a cron schedule (e.g., "run report every Monday 9am")
  • Task priorities — high, medium, low; high-priority tasks execute before low-priority ones
  • Task dependencies (DAG) — task B runs only after task A succeeds (workflow/pipeline orchestration)
  • Retry with backoff — configurable retry count, exponential backoff, dead-letter queue for exhausted retries
  • Task status tracking — query status: PENDING, SCHEDULED, RUNNING, SUCCEEDED, FAILED, RETRYING, DEAD
  • Cancellation — cancel a pending or scheduled task before it starts
  • Rate limiting — limit execution rate per task type (e.g., max 100 emails/sec)
  • Timeout enforcement — kill tasks that exceed max_execution_time
  • Result storage — persist task output for callers to retrieve
  • Idempotency — support idempotency keys to prevent duplicate execution

Non-Functional Requirements

  • Exactly-once execution — each task must execute exactly once (or at-least-once with idempotency)
  • Low scheduling latency — task picked up by worker within < 100 ms of its scheduled time
  • High throughput — schedule and execute millions of tasks per day
  • High availability — survive node failures with zero task loss
  • Horizontal scalability — add workers to increase execution throughput linearly
  • Durability — submitted tasks must not be lost (persisted before ACK)
  • Fairness — one tenant's burst of tasks must not starve other tenants
  • Observability — metrics, logs, distributed tracing per task execution

Out of Scope

  • Stream processing (Kafka Streams / Flink — different paradigm)
  • Full workflow DSL (Temporal's detailed workflow-as-code semantics)
  • Map-reduce / batch analytics (Spark / Hadoop territory)
02

Scale estimations

Scale Estimations

Traffic

MetricValue
Tasks submitted / second50K TPS
Tasks executed / second50K TPS (steady state, submitted ≈ executed)
Peak submission rate150K TPS (3x burst)
Delayed tasks (waiting) at any time500M
Recurring cron jobs (active definitions)10M
DAG workflows triggered / day5M
Average tasks per DAG8

Storage

MetricValue
Average task payload2 KB (serialized arguments + metadata)
Average task result1 KB
Task record (full row)~5 KB (payload + metadata + status + timestamps)
Tasks per day50K/s × 86,400 = ~4.3 billion/day
Retention period30 days
Total task records4.3B × 30 = ~130 billion records
Storage (task records)130B × 5 KB = ~650 TB
Delayed task index500M × 100 bytes = ~50 GB (fits in memory)
Cron definitions10M × 500 bytes = ~5 GB

Bandwidth

MetricValue
Task submission throughput50K/s × 2 KB = ~100 MB/s
Task dispatch throughput50K/s × 2 KB = ~100 MB/s
Result storage throughput50K/s × 1 KB = ~50 MB/s
Total cluster bandwidth~250 MB/s

Hardware Estimate

ComponentSpec
Scheduler nodes5-10 (active-active, stateless scheduling logic)
Task store (DB)20-50 nodes (sharded, SSD-backed)
Worker nodes1,000-5,000 (depending on avg task duration)
Delayed task queue3-5 nodes (Redis or custom priority queue, in-memory)
Message broker (internal)5-10 nodes (Kafka or internal queue)
03

Layered architecture

Phase 1 of 4 · Client & edge

Start with the entry points

First, define who uses the system and where requests enter. This is the boundary of the design, not yet a complete request path.

Clients
Submit a stable task ID, payload, execution time, and retry policy.
Task intake
Persist task state before acknowledging submission so accepted work survives process failure.
Architecture after phase 1. Includes Clients, Task intake. Clients connects to Task intake.
Phase 1 — Start with Clients, Task intake.SVGSVGEditable diagramEditable diagram

This first layer defines the system boundary. The next chapters add application logic and data dependencies. After examining the request paths and tradeoffs, we will add operational safeguards and bring the complete architecture together.

04

API & contracts

Phase 2 of 4 · Application & compute

Add the application logic

Keep the entry points and add the services that do the work. Give each service a clear responsibility before defining its contracts. Storage and background dependencies come next.

Scheduler
Move due tasks to ready queues while preserving priority and tenant fairness.
Dispatcher
Assign tasks to capable workers using expiring leases rather than assuming a worker is alive forever.
Architecture after phase 2. Includes Clients, Task intake, Scheduler, Dispatcher. Clients connects to Task intake. Task intake connects to Scheduler. Scheduler connects to Dispatcher.
Phase 2 — Added Scheduler, Dispatcher. Earlier layers remain in place.SVGSVGEditable diagramEditable diagram

API Design

Submit Task

EXAMPLE
POST /api/v1/tasks
Content-Type: application/json

Request:
{
  "task_type": "send_email",
  "payload": {
    "to": "user@example.com",
    "subject": "Welcome!",
    "template_id": "onboarding-v2"
  },
  "priority": "high",                    // high | medium | low
  "scheduled_at": "2026-04-04T09:00:00Z", // null = immediate
  "max_retries": 3,
  "retry_backoff": "exponential",         // exponential | fixed | linear
  "retry_delay_seconds": 60,             // base delay between retries
  "timeout_seconds": 300,                // kill if running > 5 min
  "idempotency_key": "welcome-user-123", // prevent duplicate submission
  "queue": "email-queue",                // logical queue for routing
  "metadata": {
    "tenant_id": "acme-corp",
    "correlation_id": "req-abc"
  }
}

Response (202 Accepted):
{
  "task_id": "task-7f3a9b2c",
  "status": "SCHEDULED",
  "scheduled_at": "2026-04-04T09:00:00Z",
  "created_at": "2026-04-03T10:00:00Z"
}

Create Recurring (Cron) Task

EXAMPLE
POST /api/v1/cron-tasks
Content-Type: application/json

Request:
{
  "name": "daily-report-generation",
  "cron_expression": "0 9 * * 1-5",       // 9am weekdays
  "timezone": "America/New_York",
  "task_type": "generate_report",
  "payload": {
    "report_type": "daily_sales",
    "recipients": ["team@acme.com"]
  },
  "max_retries": 2,
  "timeout_seconds": 600,
  "overlap_policy": "skip",               // skip | queue | cancel_running
  "enabled": true
}

Response (201 Created):
{
  "cron_id": "cron-8a4b1c",
  "name": "daily-report-generation",
  "next_fire_time": "2026-04-04T09:00:00Z",
  "status": "ACTIVE"
}

Submit DAG Workflow

EXAMPLE
POST /api/v1/workflows
Content-Type: application/json

Request:
{
  "workflow_name": "user-onboarding",
  "tasks": [
    {
      "task_id": "create-account",
      "task_type": "create_account",
      "payload": {"user": "alice@example.com"},
      "dependencies": []
    },
    {
      "task_id": "send-welcome-email",
      "task_type": "send_email",
      "payload": {"template": "welcome"},
      "dependencies": ["create-account"]
    },
    {
      "task_id": "setup-defaults",
      "task_type": "setup_user_defaults",
      "payload": {},
      "dependencies": ["create-account"]
    },
    {
      "task_id": "notify-sales",
      "task_type": "notify_slack",
      "payload": {"channel": "#new-users"},
      "dependencies": ["send-welcome-email", "setup-defaults"]
    }
  ]
}

Response (202 Accepted):
{
  "workflow_id": "wf-c5d2e1",
  "status": "RUNNING",
  "tasks": {
    "create-account": "PENDING",
    "send-welcome-email": "BLOCKED",
    "setup-defaults": "BLOCKED",
    "notify-sales": "BLOCKED"
  }
}

Query Task Status

EXAMPLE
GET /api/v1/tasks/{task_id}

Response (200 OK):
{
  "task_id": "task-7f3a9b2c",
  "task_type": "send_email",
  "status": "SUCCEEDED",
  "priority": "high",
  "created_at": "2026-04-03T10:00:00Z",
  "scheduled_at": "2026-04-04T09:00:00Z",
  "started_at": "2026-04-04T09:00:00.045Z",
  "completed_at": "2026-04-04T09:00:01.230Z",
  "duration_ms": 1185,
  "attempts": 1,
  "worker_id": "worker-42",
  "result": {"message_id": "msg-xyz789"},
  "metadata": {"tenant_id": "acme-corp"}
}

Cancel Task

EXAMPLE
POST /api/v1/tasks/{task_id}/cancel

Response (200 OK):
{
  "task_id": "task-7f3a9b2c",
  "status": "CANCELLED",
  "cancelled_at": "2026-04-03T12:00:00Z"
}

Behavior:
  PENDING/SCHEDULED → immediately set to CANCELLED
  RUNNING → send cancellation signal to worker;
            worker should check cancellation flag periodically
  SUCCEEDED/FAILED → 409 Conflict (already terminal)
05

Data model

Phase 3 of 4 · Data & async work

Connect data and background work

The services now need their dependencies: durable state, fast lookups, or work that continues outside a request. Separate these responsibilities rather than treating every component as a database.

Worker fleet
Execute with bounded concurrency and make side effects safe under at-least-once delivery.
Task store
Use conditional state transitions and fencing to reject stale worker completions.
Retry queue
Retry transient failures and cap attempts; dead-letter poison tasks for diagnosis.
Architecture after phase 3. Includes Clients, Task intake, Scheduler, Dispatcher, Worker fleet, Task store, Retry queue. Clients connects to Task intake. Task intake connects to Task store. Task intake connects to Scheduler. Scheduler connects to Dispatcher. Dispatcher connects to Worker fleet. Worker fleet connects to Task store. Worker fleet connects to Retry queue.
Phase 3 — Added Worker fleet, Task store, Retry queue. Earlier layers remain in place.SVGSVGEditable diagramEditable diagram

Data Model

Task Record

Task Record
Task RecordSVGSVGEditable diagramEditable diagram

Cron Task Definition

Cron Task Definition
Cron Task DefinitionSVGSVGEditable diagramEditable diagram

Workflow (DAG) Record

Workflow (DAG) Record
Workflow (DAG) RecordSVGSVGEditable diagramEditable diagram

Task State Machine

Task State Machine
Task State MachineWide diagram — scroll horizontally.SVGSVGEditable diagramEditable diagram
06

Core design decisions

Core Design Decisions

Decision 1: How to Implement the Delayed Task Queue

The hardest problem: efficiently waking up tasks at the right time among 500 million waiting tasks.

Option A: Database Polling
Option A: Database Polling
Option A: Database PollingSVGSVGEditable diagramEditable diagram
ProsCons
Simple, no extra infrastructurePolling creates constant DB load
Database is the single source of truthLatency: up to 1 second delay (poll interval)
Transactional guarantees (ACID)Doesn't scale well beyond ~10K tasks/sec
Works with any SQL databaseIndex scan on 500M rows is expensive
Option B: Redis Sorted Set (ZSET)
Option B: Redis Sorted Set (ZSET)
Option B: Redis Sorted Set (ZSET)SVGSVGEditable diagramEditable diagram
ProsCons
O(log N + K) for due task lookup — very fastRedis is in-memory — 50 GB dedicated to timers
Sub-second precision (100ms poll)Need durability strategy (AOF + replication)
Handles 500M entries efficientlyExtra infrastructure to maintain
Atomic ZRANGEBYSCORE + ZREMMust sync state between Redis and DB
Option C: Hierarchical Timing Wheel
Option C: Hierarchical Timing Wheel
Option C: Hierarchical Timing WheelSVGSVGEditable diagramEditable diagram
ProsCons
O(1) insert and O(1) fire per tickComplex implementation
Memory-efficientState lost on crash (need WAL or checkpointing)
No database polling overheadSingle-machine capacity limit
Used by Kafka, Netty, Linux kernelCascading between levels adds complexity
Recommendation: Redis Sorted Set + Database Backup
Recommendation: Redis Sorted Set + Database Backup
Recommendation: Redis Sorted Set + Database BackupSVGSVGEditable diagramEditable diagram

Decision 2: Task Dispatch — Push vs Pull

Decision 2: Task Dispatch — Push vs Pull
Decision 2: Task Dispatch — Push vs PullSVGSVGEditable diagramEditable diagram

Decision 3: Exactly-Once Execution

Decision 3: Exactly-Once Execution
Decision 3: Exactly-Once ExecutionSVGSVGEditable diagramEditable diagram
07

Request flows

Detailed Flow Diagrams

Immediate Task Execution Flow

Immediate Task Execution Flow
Immediate Task Execution FlowWide diagram — scroll horizontally.SVGSVGEditable diagramEditable diagram

Delayed Task Flow

Delayed Task Flow
Delayed Task FlowWide diagram — scroll horizontally.SVGSVGEditable diagramEditable diagram

Cron Evaluation Flow

Cron Evaluation Flow
Cron Evaluation FlowWide diagram — scroll horizontally.SVGSVGEditable diagramEditable diagram

DAG Workflow Execution Flow

DAG Workflow Execution Flow
DAG Workflow Execution FlowSVGSVGEditable diagramEditable diagram
08

Performance & caching

Separate finding due tasks from executing them. Index or bucket scheduling records by due time so each tick does not scan the whole task table. Dispatch in bounded batches, and apply per-tenant or per-queue concurrency limits so a large overdue batch does not starve newly due work.

09

Advanced design

Priority Queue Implementation

Multi-Level Priority Queues

Multi-Level Priority Queues
Multi-Level Priority QueuesSVGSVGEditable diagramEditable diagram

Multi-Tenant Fairness

Multi-Tenant Fairness
Multi-Tenant FairnessSVGSVGEditable diagramEditable diagram
10

Edge cases

Handling Edge Cases

Clock Skew in Distributed Schedulers

EXAMPLE
Problem: Multiple scheduler nodes with slightly different clocks
  Node A (clock +2s ahead): fires cron task at T-2s (too early)
  Node B (clock -1s behind): doesn't see task as due yet

Solution:
  1. NTP sync all nodes (< 100ms drift acceptable)
  2. Cron evaluator uses single source of time (DB's NOW() function)
  3. Delayed queue poller uses Redis TIME command (server-side clock)
  4. Always compare against the same clock, not local system time

Worker Dies Mid-Execution

EXAMPLE
Problem: Worker starts processing "charge_credit_card"
         → charges the card → crashes before ACK

Scenario: Task re-enqueued → new worker charges card AGAIN

Solution: Lease + Idempotency (defense in depth)

  Layer 1: Lease-based timeout
    Worker must heartbeat every 90s (lease = 5 min)
    No heartbeat in 5 min → task re-enqueued
    → Ensures no task is stuck forever

  Layer 2: Idempotency at application level
    charge_credit_card(idempotency_key="order-123-charge")
    → Stripe sees same idempotency key → returns original result
    → No double charge even if task runs twice

  Layer 3: Checkpoint for long tasks
    Long-running tasks (e.g., 30 min data pipeline):
    → Save progress to checkpoint store every N records
    → On retry, resume from last checkpoint
    → Avoids reprocessing entire dataset

Queue Buildup / Backpressure

EXAMPLE
Problem: Producers submit 100K tasks/s, workers process 50K/s
         → Queue grows unboundedly → memory/disk exhaustion

Solution: Multi-layer backpressure

  Layer 1: Per-tenant rate limiting at API gateway
    → Max 1,000 submissions/sec per tenant
    → Return 429 Too Many Requests

  Layer 2: Queue depth alerting
    → Alert when queue depth > 100K
    → Auto-scale worker fleet (k8s HPA on queue depth metric)

  Layer 3: Queue depth limit
    → Hard cap: reject new tasks when queue > 10M
    → Return 503 Service Unavailable with Retry-After header

  Layer 4: Priority shedding
    → When overloaded, drop low-priority tasks first
    → Move to overflow queue (processed when load decreases)

Cron Double-Fire Prevention

EXAMPLE
Problem: Two cron evaluator nodes both see "next_fire_time <= now"
         → Both create task instances → duplicate execution

Solution: Optimistic locking on cron record

  Evaluator A:
    UPDATE cron_tasks
    SET last_fired_at = now(),
        next_fire_time = compute_next(cron_expr)
    WHERE cron_id = 'cron-8a4b1c'
      AND next_fire_time = '2026-04-04T09:00:00Z'  ← CAS guard
    
  → If another evaluator already updated next_fire_time,
    this UPDATE affects 0 rows → skip (no duplicate)
  → Only one evaluator wins the CAS → exactly one task created

  Combined with: FOR UPDATE SKIP LOCKED
    → Multiple evaluators process different cron jobs in parallel
    → Same cron job processed by exactly one evaluator
11

Tradeoffs

Tradeoffs & Design Decisions Summary

DecisionOption AOption BChosenWhy
Delayed queueDB pollingRedis sorted setRedis ZSET + DB backupO(log N) lookups, sub-second precision, DB as durable fallback
Dispatch modelPush (scheduler assigns)Pull (worker requests)PullNatural backpressure, self-regulating, no scheduler bottleneck
Exactly-onceDistributed transactionsAt-least-once + idempotencyAt-least-once + idempotencyDistributed txns too slow/complex; idempotency achieves same result
PrioritySingle sorted queueMulti-level separate queuesMulti-level with weighted fairnessSeparate queues + BRPOP is simple; weighted prevents starvation
Task storeSQL (PostgreSQL)NoSQL (Cassandra)Sharded PostgreSQLACID for state transitions, FOR UPDATE SKIP LOCKED for safe polling
Cron evaluationDistributed lock (one evaluator)Parallel + optimistic CASParallel + CASNo single bottleneck; CAS prevents double-fire; horizontal scaling
Retry backoffFixed delayExponential + jitterExponential + full jitterPrevents thundering herd on retries; jitter spreads load
Worker healthHeartbeat to coordinatorLease-based timeoutLease-basedSimpler; no coordinator needed; worker extends own lease; timeout = dead
Multi-tenantShared queue, FIFOPer-tenant queues + round-robinPer-tenant + weighted round-robinPrevents noisy-neighbor starvation; weighted fairness by tier
Result storageIn task table (same row)Separate result storeSame row (task table)Simplicity; result is small (<1 KB); no extra infrastructure
12

Reliability & fault tolerance

Phase 4 of 4 · Operations & resilience

Add operational safeguards

With the request paths and tradeoffs established, add the controls that help detect trouble and keep the system operating. The final diagram retains every layer introduced so far.

Control plane
Reclaim expired leases and monitor scheduled lateness, queue depth, and failure rates.
Architecture after phase 4. Includes Clients, Task intake, Scheduler, Dispatcher, Worker fleet, Task store, Retry queue, Control plane. Clients connects to Task intake. Task intake connects to Task store. Task intake connects to Scheduler. Scheduler connects to Dispatcher. Dispatcher connects to Worker fleet. Worker fleet connects to Task store. Worker fleet connects to Retry queue. Task store connects to Control plane.
Phase 4 — Added Control plane. Earlier layers remain in place.SVGSVGEditable diagramEditable diagram

Retry & Error Handling

Retry Strategy

Retry Strategy
Retry StrategySVGSVGEditable diagramEditable diagram

Dead Letter Queue

Dead Letter Queue
Dead Letter QueueSVGSVGEditable diagramEditable diagram

Classifying Errors

Classifying Errors
Classifying ErrorsSVGSVGEditable diagramEditable diagram

Reliability & Fault Tolerance

Single Points of Failure & Mitigations

Single Points of Failure & Mitigations
Single Points of Failure & MitigationsSVGSVGEditable diagramEditable diagram

Failure Scenarios

EXAMPLE
Scenario 1: Worker crashes mid-task
  → Lease expires (5 min) → task re-enqueued → new worker picks up
  → Idempotency key prevents duplicate side effects
  → Worst case: 5 min delay (tunable via lease duration)

Scenario 2: Redis queue node fails
  → Redis Cluster failover to replica (~5-15s)
  → During failover: workers retry BRPOP → brief delay
  → If full Redis cluster down: schedulers fall back to DB polling
    SELECT ... WHERE status='PENDING' FOR UPDATE SKIP LOCKED
  → Tasks not lost (DB is the source of truth)

Scenario 3: Scheduler node fails
  → Other scheduler nodes pick up its cron evaluations
  → FOR UPDATE SKIP LOCKED ensures no conflicts
  → Zero impact on already-queued tasks

Scenario 4: Database primary fails
  → Automatic failover to standby (30-60s typically)
  → During failover: task submissions return 503, retry with backoff
  → Workers continue executing already-dispatched tasks
  → Tasks in Redis queue continue processing

Scenario 5: Network partition (split brain)
  → Redis min-replicas-to-write prevents split-brain writes
  → DB standby promotion only if primary unreachable by quorum
  → Workers on isolated side: leases expire, tasks re-enqueued

Data Recovery

Data Recovery
Data RecoverySVGSVGEditable diagramEditable diagram
13

Production architecture

High-Level Architecture

High-Level Architecture
High-Level ArchitectureWide diagram — scroll horizontally.SVGSVGEditable diagramEditable diagram

Component Breakdown

Component Breakdown
Component BreakdownSVGSVGEditable diagramEditable diagram

Full System Architecture (Production-Grade)

Full System Architecture (Production-Grade)
Full System Architecture (Production-Grade)Wide diagram — scroll horizontally.SVGSVGEditable diagramEditable diagram
14

Further exploration

Comparison: Task Scheduler vs Related Systems

Comparison: Task Scheduler vs Related Systems
Comparison: Task Scheduler vs Related SystemsSVGSVGEditable diagramEditable diagram
15

Interview playbook

Interview Tips

  1. Start with the task lifecycle state machine — Draw the states (SUBMITTED → SCHEDULED → PENDING → RUNNING → SUCCEEDED/FAILED/RETRYING → DEAD). This immediately shows you understand the full system.

  2. Delayed queue is the core challenge — Don't handwave "just use a queue." Explain why 500M delayed tasks need a sorted data structure (Redis ZSET), how the poller works (ZRANGEBYSCORE every 100ms), and why DB polling doesn't scale.

  3. Pull vs Push is a key tradeoff — Explain why pull is better: workers self-regulate via backpressure, no centralized scheduler bottleneck, natural load balancing. Mention BRPOP for blocking pull (no polling waste).

  4. Exactly-once is a lie (but achievable in practice) — Explain the reality: at-least-once delivery + idempotent execution = effectively exactly-once. The lease + heartbeat + idempotency key pattern is critical to understand.

  5. Retry strategy with jitter — Don't just say "retry 3 times." Explain exponential backoff (60s → 120s → 240s), why jitter prevents thundering herd, and how error classification (retryable vs permanent) saves worker capacity.

  6. Cron double-fire prevention — Multiple scheduler nodes evaluating crons = duplicate tasks. Solution: optimistic CAS on next_fire_time. Show the SQL: UPDATE ... WHERE next_fire_time = :expected. Only one node wins.

  7. DAG execution engine — Draw the dependency graph. Explain how the engine evaluates after each task completion: "are all dependencies of X met? If yes, enqueue X." Show parallel execution of independent tasks.

  8. Multi-tenant fairness — One tenant's 1M-task burst shouldn't starve others. Explain per-tenant queues with weighted round-robin dispatch. Connect to real-world: SaaS platform serving multiple customers.

  9. Worker auto-scaling — Scale workers based on queue depth, not CPU. Kubernetes HPA with custom metric: queue_depth / desired_processing_rate = target_workers. Show the feedback loop.

  10. End with observability — Task schedulers are notoriously hard to debug. Mention distributed tracing per task (submission → queue wait → execution), DLQ alerting (tasks that permanently fail need human attention), and cron fire accuracy monitoring.