← Back
Home › Systems › Data processing and pipelines

task-queue

Lets producers hand jobs to a durable broker and move on while workers take, run and acknowledge them, so the code that asks for work is decoupled from the code that does it.

RabbitMQ, SQS

Open the task-queue pagetask-queue MPMC bounded queueconcurrency: Vyukov's array queue for many producers and consumers: each slot carries a sequence number, so one CAS per operation claims a slot without locks.›d-ary heapdata-structures: Array heap with d children per node (d = 4 typical): half the binary heap's depth, siblings in one cache line, faster priority queues.›Topic matching triedata-structures: Trie over topic levels with + and # wildcards for subscription matchingDead-letter queuedistributed: A side queue for messages that keep failing: after N delivery attempts or a permanent error, park them with the reason so the main queue keeps moving.›Leasesdistributed: A lock that expires on its own: bounded-time ownership that survives crashes and partitions, given bounded clock drift and fencing tokens on writes.›Raftdistributed: Leader-based consensus on a replicated log; one leader per term, majority commit; the default for a strongly consistent replicated state machine.›Idempotency keysfinance: Client-chosen unique key per logical operation, stored with a request fingerprint and the final response, so retries replay the first result instead of acting twice.›Credit based flow controlnetworking: Receiver-granted credits/windows bound in-flight data (HTTP/2 windows, AMQP prefetch, backpressure)Fair queuingnetworking: Deficit round robin / weighted fair queuing to share capacity among tenants or flowsGroup commitstorage: Let concurrent committers share one log write and fsync; throughput grows with concurrency while each commit still waits at most about two fsyncs.›Write-ahead logstorage: Append each change to a checksummed sequential log and fsync it before touching data pages; the basis of crash durability in nearly every database.›Exponential backoff with jittersystems: Retry after delays that grow geometrically and are randomized, so failed clients spread out instead of hammering a recovering service in synchronized waves.›Hierarchical timing wheelsystems: Timers bucketed by expiry into wheels of slots, coarser wheels for far deadlines: O(1) insert and cancel for millions of timeouts.›False sharingconcurrency: Independent variables written by different cores on one cache line force the line to bounce between cores; pad or regroup hot data to fix it.›Memory orderingconcurrency: What Relaxed, Acquire, Release and SeqCst atomics actually guarantee, what they cost on x86 and ARM, and which one each pattern needs.›Fencing tokensdistributed: A number that grows with every lock grant and is checked by the protected resource, so a paused ex-holder's late write is rejected.›State-machine snapshotsdistributed: Periodically save the state a log has produced at index i so the log prefix can be deleted; recovery and catch-up become load snapshot plus replay suffix.›Cryptographic hashes (SHA-2,BLAKE3)security: Collision-resistant fingerprints of data; SHA-256 for interoperability, BLAKE3 for speed, HMAC/HKDF for keyed uses.›Futex-based mutexconcurrency: A mutex that is one atomic word in user space and enters the kernel (futex wait/wake) only under contention; how std and glibc locks work.›fsync, O_DSYNC and O_DIRECThardware-os: What fsync, fdatasync, O_DSYNC, O_DIRECT and F_FULLFSYNC really guarantee on Linux and macOS, and the exact call sequences for crash-safe files.›CRC32C checksumshardware-os: The Castagnoli CRC-32 with dedicated instructions on x86 (SSE4.2) and ARMv8. It is the standard fast integrity check for storage blocks, logs and network frames.›PRNGs (PCG, xoshiro)numerics: Small, fast, statistically strong non-cryptographic generators: PCG32 and xoshiro256**, with seeding, parallel streams and unbiased ranges.›Slab allocatorsystems: Pre-carve pages into equal-size slots per size class with an in-place free list: O(1) alloc and free, no external fragmentation, cache-warm reuse.›CPU caches and the memoryhierarchyhardware-os: Sizes, line widths and latencies of L1/L2/L3/DRAM on modern x86 and Apple M-series, and the data-layout rules that follow from them.›Copy-on-write forkhardware-os: fork() gives the child the parent's whole address space by sharing pages read-only and copying one only when someone writes it; vfork and posix_spawn skip even that.›Copy on write snapshotsstorage: Point-in-time snapshots by sharing pages/extents and copying only on writeSIMD programminghardware-os: Process 4-16 lanes per instruction with AVX2, AVX-512 or NEON via Rust std::arch, with runtime dispatch and a portable fallback.›

How it is built

Architecture overview

A task queue decouples the code that asks for work from the code that does it. Producers put a message (a job description, an event) into a broker and move on; workers take messages out, do the work and acknowledge. The system exists to make three promises that a plain in-memory list cannot: a message that was accepted is not lost when a process or machine dies, a message is handed to one worker at a time and comes back if that worker disappears, and a slow or failing consumer does not take the producers down with it. The price is the usual one for distributed delivery: after a failure a message may be delivered twice, so the safe contract is at-least-once delivery plus idempotent handlers. RabbitMQ and Amazon SQS are the general-purpose brokers; Celery, Sidekiq, asynq and BullMQ are job-queue frameworks that wrap a broker (often Redis) with retries, scheduling and priorities.

Parts:

 producer ──publish──► router ──► queue (leader) ──replicate──► followers (Raft, majority)
   ▲   ◄─confirm after   (topic     │  ▲   WAL + group commit
   │     quorum write)    trie)     │  └─ ready heap / timer wheel (priority, delay)
   │                                ▼
   │              credit window ──► worker 1..n ──ack──► queue deletes
   │                                   │
   └── idempotency key ◄── result      └─ no ack before timeout ─► redelivery (backoff)
                                                      │ after N attempts
                                                      ▼
                                              dead-letter queue

There are two products of one design: the broker (one logical service, correctness of storage and ordering matters) and the worker fleet (many processes, throughput and idempotency matter). The broker owns delivery state; the workers own side effects, and that is why side effects must tolerate repeats.

How the parts fit together

Durable store

The unit of durability is the Write-ahead log: a message is appended to a checksummed sequential log and flushed before the producer is told it was accepted. After a crash the broker replays the log to rebuild the queue contents and the set of messages already acknowledged. Because a flush per message would cap throughput at the disk's flush rate, brokers use Group commit: all messages that arrived while the last flush was running share the next one, so throughput grows with concurrency while each producer still waits about one flush. This is also the main latency knob: a broker that acknowledges before the flush is faster and can lose the most recent messages on a power failure; the choice is a per-queue setting in most systems.

Replication

One disk is not enough for a queue that should survive a machine. RabbitMQ's quorum queues replicate each queue with Raft: the leader appends to its log, followers copy, and an entry is committed once a majority has it. The effects on the contract are concrete: publisher confirms are issued only after a message has been replicated to a quorum of members (source: RabbitMQ quorum queues documentation), so "confirmed" means "survives the loss of a minority of nodes". Raft elects one leader per term and followers notice a dead leader by missing heartbeats; a leader's authority behaves like a Lease, bounded in time. Followers and clients retry with Exponential backoff with jitter so that they do not stampede a recovering node. The log cannot grow forever: State-machine snapshots save the queue state at a log index so the older prefix can be deleted. The cost of replication is write amplification and extra latency per message, which is why RabbitMQ's documentation advises against quorum queues for short-lived, high-churn queues and suggests reviewing the topology above roughly 5,000 quorum queues (source: same page).

Ready and delayed ordering

A queue needs to answer "which message next" fast. Plain FIFO is a log with a head pointer. Priority queues need a heap: a d-ary heap with d = 4 halves the depth of a binary heap and keeps siblings in one cache line, which matters when the heap holds millions of ready messages. Delayed delivery ("run this in 10 minutes"), retry timers and visibility timeouts are all timers, and a queue can hold millions of them. A Hierarchical timing wheel buckets timers by expiry in a few wheels of slots, so insert and cancel cost O(1) and expiry work is proportional to the timers that really fire. Retry delays are the heaviest user: every failed message creates a timer that is usually cancelled or replaced.

In-memory hand-off

Inside the broker process, publisher threads, the log writer and the consumer-delivery threads exchange messages through queues. A lock per message would make the lock the bottleneck, so the hot path uses a MPMC bounded queue: an array ring in which every slot carries a sequence number and each enqueue or dequeue takes one compare-and-swap. Bounded matters: a full ring is a signal to slow the producers down, not a reason to grow memory. This is an in-process structure only. It does not replace the durable store; it feeds it.

Routing

Producers rarely name the final queue. In AMQP-style brokers a message carries a routing key such as orders.eu.created, and consumers bind queues with patterns where * matches one word and # matches any number. Matching a key against thousands of bindings by scanning is too slow, so brokers keep a topic-matching-trie (no page yet): a trie over the dot-separated levels with wildcard edges, giving a lookup cost that depends on the key length and the matching branches, not on the number of bindings. Systems that only have named queues (SQS, Celery over Redis) skip this part, which is one reason they are simpler.

Fairness and back-pressure

Two different protections share this part. Fairness keeps one tenant or one queue from using all workers: fair-queuing (no page yet), such as deficit round robin, serves queues in turn with weights, so a flood of low-priority jobs from one customer cannot delay everyone else. Flow control keeps a fast sender from burying a slow receiver: with credit-based-flow-control (no page yet) the receiver grants a window, and the sender stops when it is used up. In a queue this appears as the consumer prefetch count: a worker is sent at most N unacknowledged messages. A prefetch of 1 gives perfectly even work for uneven tasks but wastes a round trip per message; a large prefetch hides latency but lets one slow worker hold messages that idle workers could have processed.

Delivery semantics

The broker hands a message to a worker and starts a clock. If the worker acknowledges, the message is deleted. If the clock runs out first, the message returns to the queue. Amazon SQS calls this the visibility timeout: a received message stays in the queue but is hidden from other consumers, 30 seconds by default, extendable per message with ChangeMessageVisibility (source: SQS developer guide). Celery's Redis transport does the same with a default visibility timeout of one hour (source: Celery documentation). Consequences:

  • A worker that crashes after doing the work but before acknowledging causes a second run. Celery states this trade-off directly: by default it acknowledges before running the task so a started task is never run again, and with acks_late it acknowledges after, which is only safe for idempotent tasks (source: Celery task documentation).
  • Therefore the handler must be repeatable: an Idempotency key stored with the result lets the second run return the first run's outcome instead of charging a card twice.
  • Failed jobs are retried with Exponential backoff with jitter. Sidekiq retries 25 times over about 21 days, then moves the job to a Dead set where it stays for 6 months before being discarded (source: Sidekiq wiki, Error Handling).
  • A message that keeps failing is a poison message. After a bounded number of deliveries it moves to a Dead-letter queue; SQS does this through a redrive policy with maxReceiveCount, and the message keeps its original enqueue timestamp so the dead-letter queue's retention period should be longer than the source queue's (source: SQS developer guide, dead-letter queues).

Data and control flow

One job from submit to done:

  1. The producer publishes with a routing key (or to a named queue) and, if the job must not run twice, an idempotency key.
  2. The router picks the queues; the leader appends the message to its log. Group commit batches it with others.
  3. Followers acknowledge the entry; once a majority has it, the leader confirms to the producer.
  4. The message enters the ready heap (or the timer wheel if it has a delay).
  5. A worker with free credit (prefetch slot) is chosen, the message is sent and a visibility clock starts.
  6. The worker runs the handler, checks the idempotency key, performs the side effect and acknowledges.
  7. The queue marks the message done; the log prefix is later truncated after a snapshot.

When the worker dies after step 5:

  1. No acknowledgement arrives before the timeout.
  2. The message becomes ready again with an incremented delivery count, delayed by a backoff timer.
  3. Another worker runs it; the idempotency key makes a repeated side effect harmless.
  4. After the maximum delivery count the message goes to the dead-letter queue with the failure reason, and an alert fires.

Scaling

Limits, in the order they bite, and the lever for each:

  1. Flush rate of one log. One partition or queue is bounded by one leader's sequential write path. Levers: group commit, batching on the publisher side, fast local SSDs, shard work over many queues.
  2. Number of queues. Per-queue overhead (a log, a Raft group, timers) limits how many queues one cluster carries; RabbitMQ's documentation suggests reviewing the topology above roughly 5,000 quorum queues. Lever: fewer, larger queues with a priority or tenant field, or streams.
  3. In-flight messages. Unacknowledged messages are state the broker must remember. SQS limits a standard queue to about 120,000 in-flight messages (source: SQS developer guide); beyond that, receives fail or return nothing. Levers: smaller prefetch, delete promptly, spread over several queues.
  4. Worker throughput. Workers scale out by adding processes, as long as handlers are idempotent and do not share a hot row. Lever: autoscale on queue depth and age of the oldest message, not on CPU.
  5. Hot tenants. One tenant's burst fills the queue. Levers: per-tenant queues with fair scheduling, rate limits at publish time, separate priority classes.
  6. Timers. Millions of retry and delay timers cost memory; a timing wheel keeps insert and cancel O(1), and a cap on retry attempts bounds the total.

Failure modes

FailureSymptomCauseFix
Duplicate executionside effect happens twicevisibility timeout shorter than the job, or a crash between work and acktimeout longer than the p99 job time or heartbeat extension; idempotency keys
Lost messagejob never runsacknowledged to the producer before the log flush, or unreplicated queue lost with its nodeconfirm after durable (quorum) write; replicated queue types
Poison messagesame message fails again and again, burning workersbug or bad payload, retried without limitmax delivery count, dead-letter queue, alert on its depth
Retry stormrecovery is slower than the outageall failed jobs retry at the same instantexponential backoff with jitter; retry budget per queue
Head-of-line blockingfast jobs wait behind slow onesone queue, large prefetch, uneven job timesseparate queues by job class, small prefetch, per-class worker pools
Starved tenantone customer's backlog delays otherssingle shared FIFOfair queuing by tenant, per-tenant limits
Split-brain deliverytwo leaders both deliverstale leader after a partitionconsensus-based leadership, fenced leases; follow the majority
Unbounded backlogmemory or disk fills, latency rises for hoursconsumers slower than producers, no back-pressurebounded queues, producer rate limits, drop or shed policy, alert on oldest-message age
Ordering assumption brokensteps of one workflow run out of orderretries and parallel workers reorder messagesdesign handlers order-independent, or use ordered queue types where available

Real implementations

namewhat it isrepolicencenotes
RabbitMQAMQP broker with Raft-based quorum queues and streamsrabbitmq/rabbitmq-serverMPL-2.0 and Apache-2.0 (per repo page)routing keys, publisher confirms, dead-lettering
Amazon SQSmanaged queue with visibility timeout and redrive to dead-letter queuesclosed service, documentationproprietary servicestandard and FIFO queues
NATS JetStreampersistence layer on NATS, acknowledgement-based redeliverynats-io/nats-serverApache-2.0at-least-once delivery on top of core NATS
CeleryPython distributed task queue over RabbitMQ or Rediscelery/celeryBSD-3-Clauseacks_late, visibility timeout on Redis
SidekiqRuby background jobs on Redis, retry and dead setsidekiq/sidekiqsee repo25 retries over about 21 days
asynqGo task queue on Redis: scheduling, retries, priorities, de-duplicationhibiken/asynqMITat-least-once execution, recovery after worker crash
BullMQqueue for Node.js, Python, .NET, Elixir, Rust and PHP on Redis or PostgreSQLtaskforcesh/bullmqMITbatch processing, job flows

Sources

  • RabbitMQ: Quorum queues (Raft replication, publisher confirms after quorum, about 5,000 queue guidance).
  • Amazon SQS developer guide: Visibility timeout (30 s default, about 120,000 in-flight), Dead-letter queues (redrive policy, maxReceiveCount, retention).
  • Celery: Tasks (acks_late and idempotency), Redis broker (default visibility timeout 1 hour).
  • Sidekiq wiki: Error Handling (25 retries, Dead set).
  • asynq: README (feature list).
  • NATS: JetStream (at-least-once delivery by acknowledgement).

Similar systems

Share the most building blocks with this one.