A Distributed Task Queue is a system that manages the asynchronous execution of tasks across multiple worker nodes. It decouples task producers (who submit work) from task consumers (who execute work), enabling systems to handle background jobs, scheduled tasks, and workloads that would otherwise block user-facing operations.
The core idea is straightforward: instead of executing expensive operations synchronously, you push them onto a queue and let workers process them independently. This pattern is fundamental to building scalable, resilient systems.
Popular Examples: Celery (Python), Sidekiq (Ruby), Amazon SQS, RabbitMQ, Apache Kafka
In this article, we will explore the high-level design of a distributed task queue.
Let's start by clarifying the requirements.
Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions, clarify ambiguities, and define the system's scope more precisely.
Here is an example of how a discussion between the candidate and the interviewer might unfold:
Candidate: "What is the expected scale? How many tasks per day do we need to handle?"
Interviewer: "Let's design for 100 million tasks per day, with peaks during business hours."
Candidate: "What types of tasks will the system handle? Are they short-running or long-running?"
Interviewer: "Mostly short tasks (under 30 seconds), but we should support tasks up to 15 minutes."
Candidate: "Do we need to guarantee task execution? What happens if a worker crashes mid-task?"
Interviewer: "Yes, we need at-least-once delivery. Tasks should be retried if a worker fails."
Candidate: "Should we support task prioritization and delayed execution?"
Interviewer: "Yes, we need multiple priority levels and the ability to schedule tasks for future execution."
Candidate: "Do clients need to track task status and retrieve results?"
Interviewer: "Yes, clients should be able to check if a task is pending, running, completed, or failed."
After gathering the details, we can summarize the key system requirements.
To understand the scale of our system, let's make some reasonable assumptions.
100,000,000 / 86,400 ≈ 1,150 QPS (steady state)500,000,000 / 86,400 ≈ 5,800 QPS (steady state)86,400 / 5 = 17,280 tasks100,000,000 / 17,280 ≈ 5,800 workersEach task stores:
Total ≈ 1.2 KB per task
Monthly storage: 100M tasks/day * 30 days * 1.2 KB ≈ 3.6 TB for task metadata.
The distributed task queue needs APIs for task submission, status tracking, and management. Below are the core APIs required for basic functionality.
POST /tasksSubmits a new task to the queue for execution.
400 Bad Request: Invalid payload or unknown task type.429 Too Many Requests: Rate limit exceeded.409 Conflict: Duplicate idempotency key with different payload.GET /tasks/{task_id}Retrieves the current status and details of a task.
pending: Task is waiting in the queue.scheduled: Task is delayed and waiting for its scheduled time.running: Task is currently being executed by a worker.completed: Task finished successfully.failed: Task failed after exhausting retries.dead: Task moved to dead letter queue.404 Not Found: Task ID does not exist.DELETE /tasks/{task_id}Cancels a pending or scheduled task. Running tasks cannot be cancelled.
404 Not Found: Task ID does not exist.409 Conflict: Task is already running or completed.POST /internal/tasks/fetchWorkers call this endpoint to fetch tasks for execution. This is an internal API not exposed to clients.
At a high level, our system must satisfy three core requirements:
The system has an interesting asymmetry: task submissions are relatively lightweight writes, but task distribution requires careful coordination to ensure exactly one worker picks up each task.
Note: Instead of presenting the full architecture at once, we'll build it incrementally by addressing one requirement at a time. This approach is easier to follow and mirrors how you would explain the design in an interview.
Clients need to submit tasks reliably. Even if internal components are temporarily unavailable, we should accept and durably store the task.
The entry point for all client requests. Handles authentication, rate limiting, and request routing.
It authenticates and authorizes clients, applies rate limiting per client or tenant, and routes requests to the appropriate service.
Handles all task-related operations: submission, status queries, and cancellation.
It validates task payloads against registered schemas, generates unique task IDs, persists task metadata to the database, and enqueues tasks for processing.
Stores task metadata, status, and results. This is the source of truth for task state.
It durably stores all task information, supports efficient queries by task ID and status, and handles high write throughput for task submissions.
The three components form a simple chain: the client sends the request, the Gateway validates it, and the Task Service writes the task to the database before returning the task ID.
POST /tasks request with the task payload.At this point, the task is durably stored but not yet available for workers. We need a mechanism to make it available for execution.
Workers need to efficiently discover and claim tasks. This is the most challenging part of the design because we must ensure each task is processed by exactly one worker with no duplicates, tasks are distributed fairly across workers, and high-priority tasks are processed before low-priority ones.
A distributed queue that holds tasks ready for execution. Workers pull tasks from this queue.
It stores tasks until workers are ready to process them, supports multiple priority levels through separate queues per priority, and provides a visibility timeout to handle worker failures.
Technology choices: Redis, Amazon SQS, RabbitMQ, or Apache Kafka.
Handles delayed tasks and periodic scheduling. Moves tasks from "scheduled" state to the message queue when their time arrives.
It tracks tasks scheduled for future execution, moves delayed tasks to the queue at the scheduled time, and handles recurring or cron-style tasks where supported.
Two paths lead to the Message Queue: the Task Service enqueues immediate tasks directly, while the Scheduler polls the database for delayed tasks whose scheduled time has arrived and feeds them into the same queue.
scheduled_at timestamp.scheduled_at <= now().Workers must reliably execute tasks and handle failures gracefully.
Stateless processes that fetch tasks from the queue, execute them, and report results.
Each worker fetches tasks from the message queue, executes task logic based on task type, reports success or failure back to the Task Service, and handles graceful shutdown without losing tasks.
A separate queue for tasks that have failed permanently (exceeded retry limits).
It stores failed tasks for manual inspection, preserves full task context for debugging, and allows manual retry or deletion.
The solid arrows trace the success path from queue to worker to status update, while the dashed arrow marks the failure path that routes a task to the Dead Letter Queue once retries are exhausted.
Here is the complete architecture combining all requirements:
To choose the right database for our needs, let's consider some factors:
Given these points, a SQL database like PostgreSQL is a solid choice. It offers strong consistency guarantees for status updates, efficient indexing for status-based queries, and mature tooling for operations and monitoring.
For very high scale (billions of tasks), a NoSQL database like Cassandra or DynamoDB may be more appropriate, with the trade-off of eventual consistency.
Hybrid approach: Use PostgreSQL for task metadata and Redis for the actual queuing mechanism. This combines the durability of SQL with the speed of in-memory queues.
Stores all task metadata and state.
Indexes:
task_idstatus for monitoring queriesscheduled_at for scheduler queriesidempotency_key (where not null)(status, priority, created_at) for queue orderingStores execution attempts for debugging and auditing.
Stores tasks that have permanently failed.
Now that we have the high-level architecture and database schema in place, let's dive deeper into some critical design choices.
How workers receive tasks is fundamental to the system's performance and reliability. The right strategy depends on your scale, latency requirements, and infrastructure.
Workers actively poll the queue for new tasks at regular intervals.
Instead of short polls with sleep intervals, use long polling: the worker's fetch request blocks until either a task is available or a timeout expires.
This reduces empty polls while keeping latency low.
The queue actively pushes tasks to workers as they become available.
Combine the best of both approaches: use push notifications to wake workers, but let workers pull the actual tasks.
This provides the low latency of push with the backpressure control of pull.
Recommendation: Start with long polling for simplicity. Move to a hybrid approach if you need sub-second latency at scale.
Message delivery semantics determine what happens when failures occur. This is one of the most important design decisions for a task queue.
Each task is delivered to a worker at most one time. If the worker crashes, the task is lost.
Each task is guaranteed to be delivered at least once, but may be delivered multiple times.
The visibility timeout is critical for at-least-once delivery:
Best practice: Set the visibility timeout to 2-3x the expected task duration. For variable-duration tasks, allow workers to extend their lease.
Since tasks may be delivered multiple times, your workers must be idempotent. Common strategies:
Each task is processed exactly once, regardless of failures. This is the holy grail but is extremely difficult to achieve in distributed systems.
True exactly-once delivery requires coordination between the queue and the processing logic, which typically means:
In practice, "exactly-once" is usually implemented as "at-least-once delivery with idempotent processing."
Recommendation: Use at-least-once delivery with idempotent task handlers. This provides strong guarantees without the complexity of true exactly-once semantics.
Failures are inevitable in distributed systems. A well-designed retry strategy ensures tasks eventually succeed while avoiding infinite loops and system overload.
Increase the delay between retries exponentially to avoid overwhelming failing dependencies.
Formula: delay = base_delay * (multiplier ^ attempt)
Add randomness to prevent synchronized retries from many failed tasks hitting the system simultaneously.
Set a limit to prevent infinite retry loops. Common values are 3-5 retries.
Not all errors should be retried. Classify errors and handle them appropriately:
Tasks that exhaust all retries are moved to a Dead Letter Queue for manual inspection.
Some tasks can crash workers (out-of-memory, infinite loops). Detect and isolate these:
When your queue handles different types of tasks, you need mechanisms to ensure important tasks are processed first while still making progress on lower-priority work.
The simplest approach: maintain separate queues for each priority level.
Dedicated workers: Assign specific workers to each priority level.
Weighted polling: Workers poll high-priority queues more frequently.
Priority draining: Workers always drain higher-priority queues first.
If high-priority tasks constantly arrive, low-priority tasks may never execute. Strategies to prevent this:
Some tasks should be processed by specific workers (e.g., tasks for the same user should go to the same worker for caching benefits).
Route tasks to workers based on a hash of the task's affinity key:
This ensures tasks for the same user usually go to the same worker, while still distributing load evenly.
As task volume grows, different components require different scaling approaches.
The Task Service is stateless and can be horizontally scaled behind a load balancer.
Different queue technologies scale differently:
Workers are the most common scaling target. Considerations:
The Task Database can become a bottleneck at high scale.
Read scaling: Add read replicas for status queries. Write scaling: Shard by task ID or tenant ID. Archive old data: Move completed tasks to cold storage after a retention period.
For global availability, deploy the task queue in multiple regions:
While true exactly-once delivery is difficult, we can achieve effective exactly-once processing through idempotency.
Clients provide a unique key with each task submission. The system uses this key to detect and reject duplicates.
For tasks that modify external systems, the worker must ensure idempotency:
For some use cases, you may want to deduplicate tasks within a time window (e.g., don't send more than one notification per user per hour).
notify:user123).20 quizzes