AlgoMaster Logo

Design Distributed Task Queue

21 min readUpdated June 23, 2026
Listen to this chapter
Unlock Audio

In this article, we will explore the high-level design of a distributed task queue.

Let's start by clarifying the requirements.

1. Clarifying 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:

After gathering the details, we can summarize the key system requirements.

Functional Requirements
  1. Task Submission: Clients can submit tasks with a payload and optional configuration (priority, delay, timeout).
  2. Task Execution: Workers pull tasks from the queue and execute them.
  3. Task Status: Clients can query the status of a submitted task.
  4. Retry on Failure: Failed tasks are automatically retried with configurable backoff.
  5. Priority Queues: Support multiple priority levels (high, normal, low).
  6. Delayed Tasks: Schedule tasks to execute at a future time.
  7. Dead Letter Queue: Tasks that exceed retry limits are moved to a DLQ for inspection.
Non-Functional Requirements
  1. High Availability: The system must be highly available (99.99% uptime).
  2. At-Least-Once Delivery: Every task must be executed at least once.
  3. Low Latency: Task pickup latency should be under 100ms for high-priority tasks.
  4. Scalability: Handle 100 million tasks per day with horizontal scaling.
  5. Durability: Submitted tasks must not be lost, even during failures.
  6. Ordering: Best-effort ordering within the same priority level (not strict FIFO).

2. Back-of-the-Envelope Estimation

To understand the scale of our system, let's make some reasonable assumptions.

Task Submissions (Writes)

  • Average write QPS = 100,000,000 / 86,4001,150 QPS (steady state)
  • Peak write load (3x factor) ≈ 3,500 QPS

Task Status Queries (Reads)

  • Assume a 5:1 read/write ratio (clients check status multiple times per task)
  • Status queries: 500 million per day
  • Average read QPS = 500,000,000 / 86,4005,800 QPS (steady state)
  • Peak read load (3x factor) ≈ 17,500 QPS

Worker Throughput

  • Tasks per worker per day = 86,400 / 5 = 17,280 tasks
  • Workers needed = 100,000,000 / 17,2805,800 workers
  • With overhead and buffer ≈ 8,000-10,000 workers

Storage (30 days retention)

Each task stores:

  • Task ID (~36 bytes, UUID)
  • Payload (avg 1 KB)
  • Metadata (~200 bytes: status, timestamps, retry count)

Total ≈ 1.2 KB per task

Monthly storage: 100M tasks/day * 30 days * 1.2 KB3.6 TB for task metadata.

3. Core APIs

The distributed task queue needs APIs for task submission, status tracking, and management. Below are the core APIs required for basic functionality.

1. Submit Task

Endpoint: POST /tasks

Submits a new task to the queue for execution.

Request Parameters
  • task_type (required): Identifier for the type of task (e.g., "send_email", "process_image").
  • payload (required): JSON object containing task-specific data.
  • priority (optional): Priority level ("high", "normal", "low"). Default: "normal".
  • delay_seconds (optional): Delay before task becomes eligible for execution.
  • timeout_seconds (optional): Maximum execution time before task is considered failed.
  • max_retries (optional): Maximum retry attempts. Default: 3.
  • idempotency_key (optional): Client-provided key for deduplication.
Sample Response
Error Cases
  • 400 Bad Request: Invalid payload or unknown task type.
  • 429 Too Many Requests: Rate limit exceeded.
  • 409 Conflict: Duplicate idempotency key with different payload.

2. Get Task Status

Endpoint: GET /tasks/{task_id}

Retrieves the current status and details of a task.

Sample Response
Status Values
  • 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.
Error Cases
  • 404 Not Found: Task ID does not exist.

3. Cancel Task

Endpoint: DELETE /tasks/{task_id}

Cancels a pending or scheduled task. Running tasks cannot be cancelled.

Sample Response
Error Cases
  • 404 Not Found: Task ID does not exist.
  • 409 Conflict: Task is already running or completed.

4. Internal: Fetch Tasks (Worker API)

Endpoint: POST /internal/tasks/fetch

Workers call this endpoint to fetch tasks for execution. This is an internal API not exposed to clients.

Request Parameters
  • worker_id (required): Unique identifier for the worker.
  • task_types (required): List of task types this worker can handle.
  • batch_size (optional): Maximum number of tasks to fetch. Default: 1.
Sample Response

4. High-Level Design

At a high level, our system must satisfy three core requirements:

  1. Task Ingestion: Accept tasks from clients and persist them durably.
  2. Task Distribution: Efficiently distribute tasks to available workers.
  3. Task Execution: Execute tasks reliably with proper failure handling.

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.

4.1 Requirement 1: Task Ingestion

Clients need to submit tasks reliably. Even if internal components are temporarily unavailable, we should accept and durably store the task.

Components Needed

API Gateway

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.

Task 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.

Task Database

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.

Flow: Submitting a Task

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.

  1. Client sends a POST /tasks request with the task payload.
  2. The API Gateway authenticates the request and applies rate limiting.
  3. The Task Service receives the request and: validates the payload against the task type schema, generates a unique task ID (UUID) and persists the task with status "pending" to the Task Database.
  4. The service returns the task ID to the client.

At this point, the task is durably stored but not yet available for workers. We need a mechanism to make it available for execution.

4.2 Requirement 2: Task Distribution

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.

Additional Components Needed

Message Queue

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.

Scheduler Service

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.

Flow: Making Tasks Available

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.

For immediate tasks

  1. After persisting to the database, the Task Service enqueues the task to the Message Queue.
  2. The task is now available for workers to pick up.

For delayed tasks

  1. The Task Service persists the task with status "scheduled" and a scheduled_at timestamp.
  2. The Scheduler Service periodically scans for tasks where scheduled_at <= now().
  3. The scheduler enqueues these tasks to the Message Queue and updates their status to "pending".

4.3 Requirement 3: Task Execution

Workers must reliably execute tasks and handle failures gracefully.

Additional Components Needed

Workers

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.

Dead Letter Queue (DLQ)

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.

Flow: Executing a Task

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.

  1. Worker polls the Message Queue for available tasks.
  2. The queue returns a task with a visibility timeout (e.g., 60 seconds). During this window, no other worker can see this task.
  3. The worker calls the Task Service to mark the task as "running".
  4. The worker executes the task logic.
  5. On success: Worker calls the Task Service to mark the task as "completed" with the result. The task is deleted from the queue.
  6. On failure: Worker calls the Task Service to mark the task as "failed".
  • If retries remain, the task is re-enqueued with exponential backoff.
  • If retries exhausted, the task is moved to the Dead Letter Queue.

4.4 Putting It All Together

Here is the complete architecture combining all requirements:

Core Components Summary

ComponentPurpose
API GatewayAuthentication, rate limiting, request routing
Task ServiceTask CRUD operations, status management
Task DatabaseDurable storage for task metadata and results
Redis CacheFast status lookups, idempotency key storage
Message QueuesTask distribution to workers (per priority)
SchedulerDelayed task management, moves tasks to queues
WorkersTask execution, result reporting
Dead Letter QueueStorage for permanently failed tasks

5. Database Design

5.1 SQL vs NoSQL

To choose the right database for our needs, let's consider some factors:

  • We need to store hundreds of millions of task records.
  • Most operations are simple: insert, update status, lookup by task ID.
  • We need efficient queries by status (for monitoring and cleanup).
  • Strong consistency is important for task state transitions.
  • The schema is relatively fixed and well-defined.

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.

5.2 Database Schema

1. Tasks Table

Stores all task metadata and state.

Scroll
FieldTypeDescription
task_idUUID (PK)Unique identifier for the task
task_typeVARCHAR(100)Type of task (e.g., "send_email")
payloadJSONBTask-specific data
statusVARCHAR(20)Current status (pending, running, completed, failed, dead)
priorityVARCHAR(10)Priority level (high, normal, low)
created_atTIMESTAMPWhen the task was submitted
scheduled_atTIMESTAMPWhen the task should be executed
started_atTIMESTAMPWhen execution began
completed_atTIMESTAMPWhen execution finished
timeout_secondsINTEGERMaximum execution time
max_retriesINTEGERMaximum retry attempts
retry_countINTEGERCurrent retry count
last_errorTEXTError message from last failure
resultJSONBResult data (on success)
worker_idVARCHAR(100)ID of worker executing the task
idempotency_keyVARCHAR(255)Client-provided deduplication key

Indexes:

  • Primary key on task_id
  • Index on status for monitoring queries
  • Index on scheduled_at for scheduler queries
  • Unique index on idempotency_key (where not null)
  • Composite index on (status, priority, created_at) for queue ordering

2. Task History Table

Stores execution attempts for debugging and auditing.

Scroll
FieldTypeDescription
idBIGSERIAL (PK)Auto-incrementing ID
task_idUUID (FK)Reference to the task
attemptINTEGERAttempt number (1, 2, 3...)
worker_idVARCHAR(100)Worker that processed this attempt
started_atTIMESTAMPWhen this attempt started
ended_atTIMESTAMPWhen this attempt ended
statusVARCHAR(20)Outcome (success, failure, timeout)
errorTEXTError message if failed

3. Dead Letter Queue Table

Stores tasks that have permanently failed.

Scroll
FieldTypeDescription
idBIGSERIAL (PK)Auto-incrementing ID
task_idUUIDOriginal task ID
task_typeVARCHAR(100)Type of task
payloadJSONBOriginal payload
failure_reasonTEXTWhy the task permanently failed
retry_countINTEGERNumber of attempts made
moved_atTIMESTAMPWhen moved to DLQ
resolved_atTIMESTAMPWhen manually resolved (if applicable)

6. Design Deep Dive

Now that we have the high-level architecture and database schema in place, let's dive deeper into some critical design choices.

6.1 Task Distribution Strategies

How workers receive tasks is fundamental to the system's performance and reliability. The right strategy depends on your scale, latency requirements, and infrastructure.

Approach 1: Pull-Based (Polling)

Workers actively poll the queue for new tasks at regular intervals.

How It Works

  1. Each worker runs a loop that periodically calls the queue's "fetch" operation.
  2. If tasks are available, the worker receives one or more tasks.
  3. If the queue is empty, the worker waits and retries after a delay.

Pros

  • Simple implementation: Workers control their own pace.
  • Natural backpressure: Workers only fetch when ready.
  • Fault tolerant: Worker crashes don't affect the queue.

Cons

  • Latency: Tasks may wait up to the polling interval before pickup.
  • Wasted resources: Empty polls consume network and CPU.
  • Thundering herd: Many workers polling simultaneously can overload the queue.

Optimization: Long Polling

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.

Approach 2: Push-Based (Event-Driven)

The queue actively pushes tasks to workers as they become available.

How It Works

  1. Workers establish a persistent connection to the queue (WebSocket, gRPC stream, or message broker subscription).
  2. When a task arrives, the queue immediately pushes it to an available worker.
  3. The worker processes the task and acknowledges completion.

Pros

  • Low latency: Tasks are delivered immediately.
  • Efficient: No wasted polling requests.
  • Real-time: Ideal for time-sensitive tasks.

Cons

  • Complex connection management: Must handle disconnections and reconnections.
  • Backpressure challenges: Workers can be overwhelmed if pushed too fast.
  • Stateful broker: The queue must track worker connections.

Combine the best of both approaches: use push notifications to wake workers, but let workers pull the actual tasks.

How It Works

  1. Workers subscribe to a lightweight notification channel.
  2. When tasks arrive, the queue sends a "tasks available" signal.
  3. Workers fetch tasks at their own pace upon receiving the signal.

This provides the low latency of push with the backpressure control of pull.

Summary and Recommendation

Scroll
StrategyLatencyComplexityBackpressureBest For
Pull (Polling)HigherLowExcellentSimple systems, batch processing
Pull (Long Polling)MediumLowExcellentMost general-purpose queues
PushLowestHighChallengingReal-time, time-critical tasks
HybridLowMediumGoodLarge-scale production systems

Recommendation: Start with long polling for simplicity. Move to a hybrid approach if you need sub-second latency at scale.

6.2 Delivery Guarantees

Message delivery semantics determine what happens when failures occur. This is one of the most important design decisions for a task queue.

At-Most-Once Delivery

Each task is delivered to a worker at most one time. If the worker crashes, the task is lost.

How It Works

  1. Worker fetches a task from the queue.
  2. The task is immediately deleted from the queue.
  3. Worker processes the task.
  4. If the worker crashes mid-processing, the task is gone.

When to Use

  • Tasks are not critical (e.g., analytics events).
  • Duplicate processing is worse than missing tasks.
  • Tasks are cheap to regenerate.

At-Least-Once Delivery

Each task is guaranteed to be delivered at least once, but may be delivered multiple times.

How It Works

  1. Worker fetches a task from the queue.
  2. The task becomes invisible to other workers (visibility timeout).
  3. Worker processes the task.
  4. On success: Worker explicitly deletes the task from the queue.
  5. On failure/timeout: The task becomes visible again and another worker picks it up.

The Visibility Timeout

The visibility timeout is critical for at-least-once delivery:

  • Too short: Tasks become visible while still being processed, causing duplicates.
  • Too long: Failed tasks take too long to be retried.

Best practice: Set the visibility timeout to 2-3x the expected task duration. For variable-duration tasks, allow workers to extend their lease.

Handling Duplicates

Since tasks may be delivered multiple times, your workers must be idempotent. Common strategies:

  1. Idempotency keys: Check if the task was already processed before executing.
  2. Database constraints: Use unique constraints to prevent duplicate side effects.
  3. Conditional updates: Use optimistic locking to detect concurrent processing.

Exactly-Once Delivery

Each task is processed exactly once, regardless of failures. This is the holy grail but is extremely difficult to achieve in distributed systems.

Why It's Hard

True exactly-once delivery requires coordination between the queue and the processing logic, which typically means:

  1. Transactional outbox: The task completion and side effects must be in the same transaction.
  2. Two-phase commit: Coordinate between the queue and external systems.
  3. Idempotent consumers: Make duplicate processing have no additional effect.

In practice, "exactly-once" is usually implemented as "at-least-once delivery with idempotent processing."

Recommendation

Scroll
GuaranteeComplexityData SafetyBest For
At-Most-OnceLowestLowestNon-critical, fire-and-forget
At-Least-OnceMediumHighMost production systems
Exactly-OnceHighestHighestFinancial transactions, critical workflows

Recommendation: Use at-least-once delivery with idempotent task handlers. This provides strong guarantees without the complexity of true exactly-once semantics.

6.3 Handling Task Failures and Retries

Failures are inevitable in distributed systems. A well-designed retry strategy ensures tasks eventually succeed while avoiding infinite loops and system overload.

Types of Failures

  1. Transient failures: Temporary issues that may resolve on retry (network timeout, service unavailable).
  2. Permanent failures: Issues that will never succeed on retry (invalid input, missing resource).
  3. Poison pills: Tasks that crash workers (infinite loops, memory exhaustion).

Retry Strategy

1. Exponential Backoff

Increase the delay between retries exponentially to avoid overwhelming failing dependencies.

  • Attempt 1: Immediate.
  • Attempt 2: 10 seconds.
  • Attempt 3: 100 seconds.
  • Attempt 4: 1000 seconds (~17 minutes).

Formula: delay = base_delay * (multiplier ^ attempt)

2. Jitter

Add randomness to prevent synchronized retries from many failed tasks hitting the system simultaneously.

3. Maximum Retries

Set a limit to prevent infinite retry loops. Common values are 3-5 retries.

4. Retry Classification

Not all errors should be retried. Classify errors and handle them appropriately:

Scroll
Error TypeExampleRetry?
TransientConnection timeoutYes
Rate limit429 Too Many RequestsYes (with longer backoff)
Client error400 Bad RequestNo
Server error500 Internal ErrorYes (limited)
PermanentResource not foundNo

Dead Letter Queue (DLQ)

Tasks that exhaust all retries are moved to a Dead Letter Queue for manual inspection.

DLQ Best Practices

  1. Preserve context: Store the original payload, all error messages, and retry history.
  2. Alerting: Monitor DLQ depth and alert when tasks accumulate.
  3. Manual replay: Provide tools to retry DLQ tasks after fixing the underlying issue.
  4. Retention: Keep DLQ tasks for a reasonable period (7-30 days) before deletion.

Poison Pill Detection

Some tasks can crash workers (out-of-memory, infinite loops). Detect and isolate these:

  1. Attempt counter: Track how many times a task has been attempted.
  2. Worker health: Monitor worker crash patterns.
  3. Fast-fail: If a task has crashed multiple workers, move it directly to DLQ.
  4. Resource limits: Run tasks in sandboxed environments with memory and CPU limits.

6.4 Priority and Fairness

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.

Separate Queues per Priority

The simplest approach: maintain separate queues for each priority level.

Worker Assignment Strategies

Dedicated workers: Assign specific workers to each priority level.

  • Pros: Guaranteed capacity for each priority.
  • Cons: Underutilized workers when queues are empty.

Weighted polling: Workers poll high-priority queues more frequently.

  • Pros: Flexible resource allocation.
  • Cons: Complex to tune weights correctly.

Priority draining: Workers always drain higher-priority queues first.

  • Pros: Simple logic, high-priority tasks always go first.
  • Cons: Low-priority tasks may starve.

Preventing Starvation

If high-priority tasks constantly arrive, low-priority tasks may never execute. Strategies to prevent this:

  1. Aging: Gradually increase the priority of old tasks.
  2. Minimum throughput: Reserve a percentage of workers for lower priorities.
  3. Time slots: Alternate between priority levels on a schedule.

Task Affinity

Some tasks should be processed by specific workers (e.g., tasks for the same user should go to the same worker for caching benefits).

Consistent Hashing

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.

7. Follow-ups

7.1 Scaling Strategies

As task volume grows, different components require different scaling approaches.

Scaling the Task Service

The Task Service is stateless and can be horizontally scaled behind a load balancer.

Auto-scaling triggers

  • CPU utilization > 70%
  • Request latency p99 > 100ms
  • Queue depth growing faster than drain rate

Scaling the Message Queue

Different queue technologies scale differently:

TechnologyScaling Approach
RedisCluster mode with hash slots
SQSAutomatically scales, no limit
RabbitMQFederation or sharding by queue
KafkaAdd partitions, rebalance consumers

Scaling Workers

Workers are the most common scaling target. Considerations:

  1. Scale based on queue depth: Add workers when queue grows, remove when empty.
  2. Scale based on latency: Add workers when task wait time exceeds threshold.
  3. Over-provision for peaks: Maintain headroom for traffic spikes.
  4. Graceful shutdown: Allow workers to finish current tasks before termination.

Scaling the Database

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.

Multi-Region Deployment

For global availability, deploy the task queue in multiple regions:

  1. Regional queues: Tasks submitted in a region are processed in that region.
  2. Cross-region replication: Critical tasks are replicated to a backup region.
  3. Failover: If a region fails, tasks can be rerouted to another region.

7.2 Ensuring Exactly-Once Task Execution

While true exactly-once delivery is difficult, we can achieve effective exactly-once processing through idempotency.

Idempotency Keys

Clients provide a unique key with each task submission. The system uses this key to detect and reject duplicates.

Implementation

  1. Store idempotency keys in Redis with a TTL (e.g., 24 hours).
  2. On task submission, check if the key exists.
  3. If exists, return the existing task ID without creating a duplicate.
  4. If not, create the task and store the key.

Worker-Side Idempotency

For tasks that modify external systems, the worker must ensure idempotency:

  1. Check before write: Query the target system to see if the action was already performed.
  2. Conditional updates: Use version numbers or ETags to prevent duplicate writes.
  3. Unique constraints: Let the database reject duplicate operations.

Deduplication Windows

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).

Implementation

  1. Create a deduplication key from task attributes (e.g., notify:user123).
  2. Check if a task with this key was recently executed.
  3. If yes, skip or merge with the existing task.

Quiz

Design Task Queue Quiz

20 quizzes