ayushsalampuriya.xyz Revise

System Design · Design Question

Design a Task Scheduler

Cron at scale: schedule one-shot and recurring jobs, dispatch reliably, and avoid double execution when workers race.

1. Requirements

Ask: precision (1s vs 1min)? calendar TZ? priority queues? max job runtime? fan-out fan-in workflows?

2. API

POST /v1/jobs { "name": "nightly-report", "schedule": "0 3 * * *", // cron OR "runAt": iso8601 "timezone": "Asia/Kolkata", "payload": { "type": "report", "id": "R1" }, "retry": { "max": 5, "backoff": "exp" } } → { "jobId": "j9" } GET /v1/jobs/{id} DELETE /v1/jobs/{id} // cancel future runs POST /v1/jobs/{id}/pause

3. Data model

jobs(job_id, cron_or_ts, tz, payload, state, next_run_at, version) job_runs(run_id, job_id, scheduled_for, status, locked_by, locked_until, attempt) UNIQUE(job_id, scheduled_for) -- prevents duplicate run rows indexes: next_run_at WHERE state=active

4. High-level design

API → Jobs DB ^ Scheduler workers (many) loop: claim due jobs (SELECT ... FOR UPDATE SKIP LOCKED / lease) enqueue execution message compute next_run_at for cron Execution queue → Executors run handler / HTTP callback ACK success → mark run succeeded fail → retry / DLQ

5. Claiming due work (avoid double fire)

BEGIN SELECT * FROM job_runs WHERE status='ready' AND scheduled_for <= now() ORDER BY scheduled_for FOR UPDATE SKIP LOCKED LIMIT 100 UPDATE ... SET status='leased', locked_by=worker, locked_until=now()+lease COMMIT -- Only one worker gets each row

Leases expire so a crashed worker does not hold a job forever. Handlers must be idempotent because lease expiry can cause a second attempt.

6. Time and cron

7. Exactly-once illusion

True exactly-once across distributed executors is hard. Practical approach: at-least-once dispatch + idempotent business effect (store run_id processed set).

8. Scaling

9. Failure modes

Quick revision

  • Persist jobs + next_run_at; many schedulers claim with leases.
  • SKIP LOCKED / lease expiry prevents stuck and reduces doubles.
  • Execute via queue workers; retry + DLQ.
  • Idempotent handlers; unique (job_id, scheduled_for).
  • Cron computed in TZ, stored next fire in UTC.
  • At-least-once + idempotency beats fake exactly-once.
  • Jitter midnight herds; shard claim load.