POST /tasks
Request: { "type": "send_email", "payload": {...}, "run_at": "2026-06-01T09:00:00Z" }
Response: { "task_id": "t_123", "status": "PENDING" }
POST /recurring-tasks
Request: { "type": "daily_report", "payload": {...}, "interval": "24h", "start_at": "...", "max_runs": 365 }
Response: { "recurring_task_id": "rt_456", "next_run_at": "..." }
GET /tasks/{id} - fetch status, timestamps, error details, attempt count
POST /tasks/{id}/cancel - cancel a pending tasks (plus pause/resume for recurring)
GET /health or /metrics - health + queue depth, execution rate, failure rate
User calls the API -> validates and writes the task to PG DB (source of truth) -> a scheduler scans the DB for tasks due in the future and pre-loads them into Redis -> dispatcher claims due tasks from Redis and hands to workers -> workers execute and write success/failed back -> the notification service fires on completion or failure
PostgreSQL - durable
Redis - fast dispatch - instant polling
Two separate queues - DB holds ALL tasks, Redis holds the soon-to-run ones
Sharding sync for scheduler instances on task_id for scalability
A PostgreSQL DB for all tasks stored
Redis for soon to execute tasks: ZSET where score = run_at, holding tasks due int he next few minutes.
The Dispatcher - atomic task claiming (the gate)
multiple instances polling Redis - two of them could grab the same task and both execute it -> danger of duplication -> the fix is atomic claim -> status='PENDING' is the guard -> dispatcher 1 claims first, dispatcher 2 will skip -> no locking schemes or race condition protection needed
The Scheduler - near-future preloading + cold-start recovery
the scheduler scans PG DB for tasks due inn the next few minutes and pushes to Redis ZSET (score = run_at) -> Dispatchers pop tasks where score <= now()
Recurring task generation - avoid the race
2 schedulers both scan `recurring_tasks` for `next_run_at` <= now() -> both create concrete task for the same interval -> duplicate run: Fix with compare-and-swap on `next_run_at
Catch-up policy