Skip to main content
Articles/System Design/system-design-hld-job-scheduler-example
Scenario Article

System Design HLD Example: Distributed Job Scheduler

The "Ghost Task" Problem

Abstract Algorithms•Mar 28, 2026
💡 Executive Summary

A distributed job scheduler ensures tasks fire reliably using a durable Job Store with a next fire time index.

🎯 System Scenario & Requirements

System Goal

A distributed job scheduler ensures tasks fire reliably using a durable Job Store with a next fire time index.

Actors & Components

  • Edge Traffic Ingress
  • Stateless Microservices
  • Datastore Infrastructure
  • API Gateway
  • Domain Service
  • Redis Cache
  • Transactional DB

Functional Requirements

  • TLDR: A distributed job scheduler ensures tasks fire reliably using a durable Job Store with a next fire time index.
  • To handle multiple scheduler instances without double-firing, we use optimistic row-level locking ( UPDATE WHERE status='SCHEDULED' ).
  • By decoupling the Trigger Engine from the Worker Pool via a message queue (Kafka/SQS), the system achieves independent scaling and guaranteed recovery from node failures.

Constraints & Assumptions

  • The component diagram below shows how the Scheduler Cluster, Coordination Layer, and Worker Pool connect, making clear which tier scales independently under burst load.
  • Critical Failure Mode — The Stuck Job: A Worker claims a job (status = ACQUIRED), begins execution, and then the Worker process crashes mid-run.

⚡ Execution Stages & Decision Trace

1

Stage 1 — Distributed Job Scheduler: Use Cases & Requirements

Action / Decision:

Actors & Journeys - Client Application: Submits tasks to be executed later (e.g., "Send this email in 2 hours").

Component Behavior:

- Trigger Engine: The "clock" of the system that identifies which tasks are due for execution.

Observable Output: - Worker Pool: The fleet of stateless execution nodes that perform the actual work (e.g., calling an external API).
Read Full Stage in Deep Dive ↓
📖

Deep Dive

Detailed Architectural Analysis

Scenario context

A distributed job scheduler ensures tasks fire reliably using a durable Job Store with a next_fire_time index. To handle multiple scheduler instances without double-firing, we use optimistic row-level locking (UPDATE WHERE status='SCHEDULED'). By decoupling the Trigger Engine from the Worker Pool via a message queue (Kafka/SQS), the system achieves independent scaling and guaranteed recovery from…

This walkthrough focuses on The "Ghost Task" Problem.

Before making a design choice, clarify:

  • Imagine you’re building a billing system for a global SaaS platform.
  • Every month, on the 1st at midnight, your system must trigger 10 million subscription renewals.
  • If your scheduler is a simple in-memory cron job (like a basic Timer in Java or a cron tab on a single server), and the server restarts at 11:59 PM, all 10 million jobs vanish.

Concepts in play: system-design-hld-architectures, caching, database-indexing, message-queues

Stage 1 — Distributed Job Scheduler: Use Cases & Requirements

  • Actors & Journeys - Client Application: Submits tasks to be executed later (e.g., "Send this email in 2 hours").
    • Trigger Engine: The "clock" of the system that identifies which tasks are due for execution.
    • Worker Pool: The fleet of stateless execution nodes that perform the actual work (e.g., calling an external API).
    • Admin Console: Provides visibility into the job pipeline, allowing for manual retries and status monitoring.

Observable outcome: - Admin Console: Provides visibility into the job pipeline, allowing for manual retries and status monitoring.

Stage 2 — Basics: How Time-Based Triggers Work

  • At its simplest level, a scheduler is a Priority Queue where the "Priority" is the next fire time .
  • The Polling Loop: A background thread wakes up every second, queries for all jobs where next fire time <= now , and marks them for execution.
  • The Precision Problem: If your polling loop takes 2 seconds but you poll every 1 second, the polls start to overlap.
  • If your database query takes 5 seconds, your "real-time" scheduler is now lagging significantly.

Observable outcome: If your database query takes 5 seconds, your "real-time" scheduler is now lagging significantly.

Stage 3 — Mechanics: The Lifecycle of a Distributed Task

  • The life of a scheduled job follows a precise state machine to ensure reliability: 1.
  • SCHEDULED: The job is persisted in the DB and indexed in Redis.
  • ACQUIRED/RUNNING: A scheduler instance has claimed the job and is currently pushing it to the message queue.
  • DISPATCHED: The job is on the queue, waiting for a worker.
flowchart TD
    A["Submitted"] --> B["Scheduled"]
    B --> C["Acquired"]
    C --> D["Dispatched"]
    D --> E["Running"]
    E --> F{"Outcome"}
    F -->|Success| G["Completed"]
    F -->|Error| H{"Retry limit?"}
    H -->|Under limit| I["Retrying"]
    H -->|Exceeded| J["Dead Letter"]
    I --> B
    B --> K["Paused"]
    K --> B

Observable outcome: DISPATCHED: The job is on the queue, waiting for a worker.

Stage 4 — Estimations & Design Goals

  • The Math of "Midnight Bursts" - Daily Volume: 100 Million jobs/day.
    • The Hot Minute: 20% of all daily jobs cluster at 00:00 (Midnight) for daily reports and billing.
    • Burst Throughput: 20M jobs / 60 seconds $\approx \mathbf{333,000 \text{ jobs/sec}}$ to be triggered.
    • Storage Requirement: 100M jobs $\times$ 500 bytes (JSON payload + metadata) $\approx \mathbf{50 \text{ GB/day}}$.

Observable outcome: - Storage Requirement: 100M jobs $\times$ 500 bytes (JSON payload + metadata) $\approx \mathbf{50 \text{ GB/day}}$.

Stage 5 — High-Level Design: The Poll-Dispatch-Execute Pattern

  • The architecture separates the time-keeping (Trigger) from the work-execution (Worker).
  • The diagram above separates the three core roles: the Job API accepts submissions and returns 202 Accepted immediately; the Scheduler Service identifies due jobs and claims them; and the Worker Pool executes them indepe…
  • The Redis Sorted Set acts as a lightweight time index for O(log N) polling, while Postgres is the durable source of truth.
  • This architecture means each component can fail and recover independently without losing a single job.
graph TD
    Client -->|POST /jobs| API[Job API]
    API -->|Insert| DB[(Job Store: Postgres)]
    API -->|ZADD| Redis[(Redis Trigger Index)]

    subgraph Trigger_Engine
        Sched[Scheduler Service] -->|Poll| Redis
        Sched -->|CAS Lock| DB
        Sched -->|Dispatch| MQ[Message Queue: Kafka]
    end

    MQ --> Workers[Worker Pool]
    Workers -->|Heartbeat| DB
    Workers -->|Update Status| DB

Observable outcome: This architecture means each component can fail and recover independently without losing a single job.

Stage 6 — Deep Dive: How the Scheduler Achieves Exactly-Once Dispatch Without a Global Lock

  • The most subtle engineering in a distributed scheduler is the claim mechanism.
  • If multiple Scheduler instances are running for high availability, they must race to claim each job without either missing it (never fires) or both claiming it (double fires).
  • The solution is Optimistic Locking via Compare-and-Swap (CAS) UPDATE .
  • Internals: The Poll-and-Lock Cycle Every second (configurable), each Scheduler instance queries the Redis Sorted Set for all jobs with a score (next fire time as Unix epoch) less than or equal to the current time.
sequenceDiagram
    participant C as Client
    participant API as Job API
    participant DB as Job Store
    participant R as Redis ZSET
    participant S as Scheduler
    participant MQ as Kafka
    participant W as Worker
    C->>API: POST /jobs with payload and schedule
    API->>DB: INSERT job with status=SCHEDULED
    API->>R: ZADD trigger score=next_fire_time
    API-->>C: 202 Accepted
    S->>R: ZRANGEBYSCORE for due jobs
    S->>DB: CAS UPDATE status=ACQUIRED
    S->>MQ: Publish job message to topic
    MQ->>W: Deliver job to consumer
    W->>DB: UPDATE status=SUCCESS

Observable outcome: Internals: The Poll-and-Lock Cycle Every second (configurable), each Scheduler instance queries the Redis Sorted Set for all jobs with a score (next fire time as Unix epoch) less than or equal to the current time.

End-to-end outcome

After working through the stages, you should be able to explain:

  • Lesson 1 — Idempotency in Workers is not optional.
  • The system provides at-least-once delivery by design — Kafka may redeliver, the Watchdog may re-dispatch a stuck job.

Alternatives considered

The source focuses on one primary design. A second architecture is not compared, so no alternative is invented here.

Failure and degraded modes

Reliability vs. Throughput: Trade-offs in Distributed Scheduling

  • Detection: The component diagram below shows how the Scheduler Cluster, Coordination Layer, and Worker Pool connect, making clear which tier scales independently under burst load.
  • User or operator impact: The component diagram below shows how the Scheduler Cluster, Coordination Layer, and Worker Pool connect, making clear which tier scales independently under burst load.

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit