Skip to main content
queue

Message Queue

Async task queue that decouples producers from consumers and smooths bursts. Each message is processed exactly once (per-message ACK model).

Why it exists

Some work doesn't need to happen in the request/response cycle — sending a welcome email, resizing an image, computing an analytics rollup. If you do it inline, the user waits. If you drop it, you lose reliability. A message queue is the middle path: the producing service enqueues the work and returns immediately; a pool of workers pulls from the queue and processes at their own pace. This decoupling absorbs bursts and lets each side scale independently.

How it works

A queue is FIFO by default (with per-message-ACK semantics — the message stays visible until a worker explicitly acknowledges completion). Producers publish messages to a queue; workers poll the queue, process one message at a time, and ACK on success. On failure or timeout, the message becomes visible again for another worker (visibility timeout). Messages that fail N times land in a dead-letter queue for manual inspection. This differs from Kafka: queues do per-message ACK, not offset-based; consumption removes the message, not just advances a pointer.

Scaling characteristics

A single queue can handle 10K-100K messages/sec depending on message size and durability guarantees. Horizontal scale by adding partitions (like Kafka) or by splitting hot queues into topic hierarchies (per-user queues). Worker pools scale independently: 10 workers vs 1000 depending on the depth of the queue and processing time per message. Queue depth is your primary alarm — a growing queue means consumers are behind.

When to use it

  • Background jobs (email, thumbnail generation, PDF rendering)
  • Rate-smoothing bursty traffic — request comes in, task is enqueued, worker processes at bounded rate
  • Retryable work with delayed retry semantics (backoff + DLQ)
  • Fan-out from one producer to one consumer (unlike pub/sub which is 1-to-N)
  • Isolating slow-but-non-critical work from the request path

When NOT to use it

  • Sub-second delivery latency — queues typically buffer, adding ~50-500ms of latency
  • Ordered event streams with replay (use Kafka)
  • You need per-message durability guarantees stronger than what your queue offers — Kafka's disk-first design wins here
  • Cross-region low-latency — most queue services are single-region or replicate with delay

Failure modes

  • Worker crashes mid-message: visibility timeout returns the message to the queue; but if the worker committed a side effect (email sent) before crashing, you get a duplicate. Mitigate with idempotent workers.
  • Poison message stalls the queue: one bad message keeps failing and blocks workers. Mitigate with DLQ + retry limit.
  • Slow consumers create a backlog: queue depth grows, memory pressure at queue broker. Mitigate with autoscaling consumers + circuit breaker on producer.
  • Message loss on broker failure if durability isn't configured — always enable replication + fsync for critical work.
  • Fan-out amplification: one message triggers 10 downstream messages, each triggers 10 more — quickly overwhelms the queue. Mitigate with backpressure and per-tier rate limits.

Alternatives

  • Kafka — for event streaming with replay + higher throughput; different semantics (offset-based, not per-message ACK)
  • Cron + database polling — dead simple for low volume; doesn't scale but zero moving parts
  • In-process job queue (Celery, Sidekiq) — piggybacks on Redis; great for smaller scale, harder to isolate failure
  • Serverless (AWS SQS + Lambda) — managed queue + managed workers; cost-effective for spiky loads

Interview questions

  • Your queue depth is climbing. What are the possible causes and how do you diagnose?
  • A message fails 5 times. Where does it go, and what should you do about it?
  • How would you guarantee exactly-once processing for a queue where each message triggers a payment?
  • You're at 100K messages/sec on a single queue. When do you start sharding?
  • How do you handle a message that requires a service that's currently down?
  • Design a delayed-execution queue (message becomes visible after 24 hours).
The story of async work

1980s, IBM: banks need a way to talk to each other that survives crashes

Before there was "event-driven microservices", there was a very concrete problem: a bank teller in London needed to talk to a mainframe in Zurich, and neither could crash while the money moved. In 1983, IBM shipped MQ Series — a persistent, transactional message queue that survived power outages and network drops. Every enqueued message was written to disk. Every consumer explicitly acknowledged. If anything failed, replay from the log. For 30 years, this was how enterprises moved data reliably.

The open-source era started when ActiveMQ (2003) and RabbitMQ (2007) brought MQ patterns to everyone with a JVM or an Erlang install. Then in 2011, two things happened simultaneously: AWS SQS made queues serverless — no brokers to run, pay per message — and Apache Kafka reframed the queue as a durable log, not a mailbox. Consumers didn't "take" messages; they read from an offset. The old "delete after consume" assumption was gone.

Today the space is two overlapping models. The message queue model (SQS, RabbitMQ, Google Pub/Sub) offers per-message ACK, visibility timeout, and dead-letter queues — perfect for background jobs and task processing. The log model (Kafka, Pulsar, Redpanda) offers replayable offset-based consumption — perfect for event streams and audit trails. They're not competitors; they solve different problems. Most modern systems use both.

The core lesson: a queue is not a fancy list. It's a contract about failure. "If the worker crashes mid-task, the message will be redelivered." "If the network drops, the message survives." "If the same message arrives twice, my code will do the right thing." Building on top of a queue means writing code that can handle these promises being kept — retries, idempotency, and back-pressure — and knowing why they were made in the first place.

Queue vs log at a glance
Queue (SQS, RabbitMQ)
Per-message ACK. Consumption removes the message. Best for tasks.
Log (Kafka, Pulsar)
Offset-based. Messages stay for retention window. Best for events.
Pub/Sub (RabbitMQ topic, Pub/Sub, NATS)
1-to-N fan-out. Each subscriber gets a copy.
Rule of thumb: if the answer to "can I replay this in 3 days?" is yes, use a log. If "delete after done" is fine, use a queue.

Historical timeline

  1. 1983
    IBM MQ Series launches
    First transactional persistent queue. Powers banking and airline reservations for 4 decades.
  2. 1993
    TIBCO Rendezvous
    Publish-subscribe messaging for financial markets. Pattern gets codified.
  3. 2001
    JMS specification
    Java Message Service standardizes queue APIs across vendors — MQ Series, Sonic, WebSphere, WebLogic.
  4. 2003
    Apache ActiveMQ
    First widely-used open-source Java queue. JMS-compliant. Powers Camel, Mule, and countless enterprise ESBs.
  5. 2006
    AMQP protocol
    Wire protocol standard for messaging emerges — JPMorgan-led. Breaks the vendor lock-in of MQ Series.
  6. 2007
    RabbitMQ 1.0
    Erlang-based AMQP implementation. Flexible routing (topic, direct, fanout, headers). Becomes the default enterprise choice.
  7. 2011
    AWS SQS goes GA
    Serverless queue. No brokers to run. Pay per message. Democratizes async processing for every startup.
  8. 2011
    Apache Kafka open-sourced
    LinkedIn releases Kafka. Redefines the queue as a durable log. Consumers use offsets, not ACKs. Enables event sourcing at scale.
  9. 2012
    NATS by Derek Collison
    Ultra-low-latency messaging in Go. "fire and forget" for high-throughput internal communication. Powers Cloud Foundry.
  10. 2014
    Apache Pulsar
    Yahoo builds a "messaging + streaming" system. Compute + storage separation. Multi-tenant from day one. Later becomes a Kafka alternative.
  11. 2015
    Redis Streams announced
    Antirez adds a Kafka-style log to Redis. Powers Sidekiq alternatives, small-scale event streams. Simple and dev-friendly.
  12. 2017
    Google Pub/Sub GA
    Serverless publish-subscribe with global scale. Ordering keys added later (2020). Deep BigQuery integration.
  13. 2019
    SQS FIFO + exactly-once
    AWS SQS adds FIFO queues with deduplication. Message ordering + exactly-once for use cases that need it (with throughput trade-off).
  14. 2021
    Redpanda releases
    Kafka-compatible in C++. No JVM, no ZooKeeper. Lower latency. Focus on modern deployments.
  15. 2024
    AI queues emerge
    LLM providers offer queue-style APIs (OpenAI Batch API). Async LLM inference reshapes what a "job queue" looks like.

Queue vs Pub/Sub: two totally different fan-out models

Everyone says "queue" when they mean one of two things. Getting this right is the difference between "one worker did the job" and "three services all reacted to the event."

Queue (point-to-point)

Producer enqueues → exactly ONE consumer processes → done.

Producer → [ msg1 msg2 msg3 ] → Worker A (msg1)
                              → Worker B (msg2)
                              → Worker C (msg3)
Semantics: Each message processed exactly once (by one worker in the pool).
Use for: Background jobs, task processing, resize this image, send that email.
Products: SQS, RabbitMQ (direct exchange), Sidekiq, BullMQ, Celery.

Pub/Sub (topic-based)

Producer publishes to topic → ALL subscribers receive a copy → each independently processes.

Producer → [ topic:order.placed ]
             → Analytics Service (copy)
             → Email Service (copy)
             → Warehouse Service (copy)
Semantics: Each message delivered to every subscriber. 1-to-N fan-out.
Use for: Event notifications, cache invalidation, downstream reactions.
Products: Google Pub/Sub, SNS, Kafka topics, Redis Pub/Sub, NATS.
Modern hybrid: The "fanout-then-queue" pattern. Publish to SNS → SNS fans out to 5 SQS queues → each service consumes independently. Best of both. AWS default.
Kafka consumer group nuance: Kafka topics look like pub/sub, but within a single consumer group each message goes to only one consumer. Add multiple groups → each group gets its own copy. So a topic can act as either mode depending on group configuration.

Three delivery guarantees — pick your poison

Distributed messaging is one of the classic "you can't have all three" corners. The three delivery guarantees each trade something for something:

At-least-once (default in SQS, RabbitMQ, Kafka)

How: Producer waits for ack. Worker ACKs after processing. If worker crashes before ACK → message redelivered.
Pros: Never loses data. Simple retry logic. Battle-tested.
Cons: Duplicates possible. Consumers MUST be idempotent (same message twice → same outcome).
Use for: The default choice for 95% of workloads. Combined with idempotent handlers = safe.
Products: SQS Standard, RabbitMQ, Kafka default, Google Pub/Sub, Sidekiq.
The pragmatic answer: use at-least-once + write idempotent consumers. Most "exactly-once" implementations still require your business logic to be idempotent anyway (because side effects can happen outside the transaction).

Visibility timeout: how queues handle worker crashes

The magic that makes "at-least-once" delivery real is visibility timeout. When a worker takes a message, it becomes invisible to others. If the worker doesn't ACK within the timeout, the message reappears. Watch it play out:

Auto-advances every 2.5s

1. Message enqueued

Producer sends msg-42 to queue. It sits visible, waiting.

Message state
visible
Worker A
idle
Worker B
idle
Setting visibility timeout: should be > your longest expected task. Too short = double processing. Too long = slow recovery from crashes.
Consequence: after a real crash, msg-42 was received by BOTH workers (A crashed, B completed). Your worker MUST be idempotent — same message twice → same outcome.

Dead Letter Queue (DLQ): how to handle poison messages

Some messages just can't be processed — malformed JSON, missing external dependency, a customer that was deleted while the message sat in the queue. If you retry forever, one bad message blocks the queue. The answer is the DLQ: after N retries, move the message to a separate queue for humans to inspect.

Main queue
msg-42 (attempt: 1)
Retry cycles
Try 1 → fail (retry in 1s)
Try 2 → fail (retry in 4s)
Try 3 → fail (retry in 16s)
Try 4 → fail (retry in 60s)
Try 5 → still failing
Dead Letter Queue
msg-42 (5 failures)
Human review. Fix bug or delete.
Retry policy: exponential backoff (1s, 4s, 16s, 60s...) with max attempts (typically 5-10).
DLQ alarms: a growing DLQ = something is broken. Page the on-call. Fix upstream or drain manually.
DLQ reprocessing: once you fix the bug, replay DLQ messages back to main queue. Tools like Elephant do this.

Consumer groups: horizontal scaling made easy

A single queue with N worker processes forms a consumer group. Messages are load-balanced across workers automatically. Add workers → linear throughput scaling. Try it:

Workers in group:
Queue: [ msg-1, msg-2, msg-3, msg-4, msg-5, msg-6 ]
Worker 1
msg-1
msg-4
Worker 2
msg-2
msg-5
Worker 3
msg-3
msg-6
Add workers → higher throughput. 6 messages, 3 workers = 2 msgs each. 6 workers = 1 msg each. Linear scale.
Auto-rebalance. If a worker crashes, the queue redistributes its messages to survivors.
Kafka nuance: in Kafka, workers get whole partitions. Adding more workers than partitions = idle workers.

Product comparison

ProductModelTypeStrengthWeakness
AWS SQS StandardManagedQueueTrivial to use. Serverless. Auto-scale. Cheap at scale.At-least-once (dup). No ordering (Standard). Message size ≤256KB.
AWS SQS FIFOManagedQueueExactly-once + strict ordering per group. Idempotency built-in.300 msg/sec limit per API (3000 with batching). More expensive.
RabbitMQOSS + ManagedQueue/Pub-subFlexible routing (topic, direct, fanout, headers). AMQP standard. Mature.Erlang ops. Cluster complexity. Throughput ceiling ~50K/sec.
Apache KafkaOSS + ManagedLogMillions of msg/sec. Replay. Multi-consumer. Event sourcing native.Operational complexity (ZK/KRaft). Overkill for simple tasks.
Google Pub/SubManagedPub-subGlobal scale. Deep BigQuery integration. Ordering keys.GCP-only. Slower than SQS. Cost ramps.
Azure Service BusManagedQueue/Pub-subSessions (message groups). Duplicate detection. Deep .NET integration.Azure-only. Complex tier model. Message size limits.
NATS + JetStreamOSS + ManagedPub-sub/LogSub-ms latency. Simple. Cloud Native (CNCF). Global-scale via clusters.Younger than Kafka. Persistence in JetStream still evolving.
Apache PulsarOSS + ManagedLog/Queue hybridMulti-tenant. Compute + storage separation. Geo-replication native.Complex ops (BookKeeper). Smaller community than Kafka.
RedpandaOSS + EnterpriseLog (Kafka-compat)C++ rewrite. No JVM/ZK. Lower latency. Same client API as Kafka.Younger. Fewer features than Kafka. Smaller ecosystem.
Redis StreamsOSS + ManagedLogRedis simplicity. Sub-ms latency. Consumer groups + persistence. Cheap.In-memory limits. Not for petabyte replay. Single-writer per shard.
Sidekiq (Redis-backed)OSSQueueBest DX for Ruby. Millions of jobs/day. Simple.Ruby-only. Redis dependency. Not for exactly-once.
TemporalOSS + ManagedWorkflow (queue+state)Durable execution. Retries + state machine built in. Great for long workflows.Not a raw queue — different mental model. Ops complexity for self-hosted.

How to choose: AWS + simple tasks → SQS Standard. Financial ordering → SQS FIFO. Enterprise routing → RabbitMQ. Event streams + replay → Kafka. GCP-native → Pub/Sub. Ultra-low latency internal → NATS. Ruby monolith → Sidekiq. Long workflows → Temporal.

12 real-world message queue deployments

Amazon

SQS runs trillions of messages/day

SQS is one of AWS's oldest services (2004 internal, 2006 public). Every AWS customer uses it — order processing, workflow steps, retries. Trillions of messages/day. Behind the scenes: massive distributed storage with per-message ACK semantics + visibility timeout as the correctness primitive.

Shopify

Sidekiq processes billions of Ruby jobs/day

Shopify runs its Ruby background jobs on Sidekiq — Redis-backed queues. Every product update, inventory sync, email send goes through it. During Black Friday: 76M req/min surface, but many more billions of async jobs. Sidekiq Pro used for reliability.

Uber

SQS for driver-rider matching

Uber uses SQS at massive scale for dispatch — when a rider requests a ride, the request enters a queue that dispatchers (partitioned by geographic hex) pull from. Absorbs surge pricing storms. Handles retries when GPS drops.

Airbnb

RabbitMQ for booking workflow

Airbnb uses RabbitMQ for the multi-step booking workflow — hold inventory, charge card, notify host, send confirmation. Each step is a queue. Failure at any step triggers rollback via compensating messages. Classic saga pattern.

Twitch

Kafka + SQS for chat delivery

Twitch uses Kafka for chat message events (need replay + massive fanout to viewers) plus SQS for moderation actions (need ACK + retry semantics). Different tools per shape.

Instagram

Celery + Redis for async work

Instagram runs Celery (Python task queue) with Redis broker for backend jobs — feed refresh, notification delivery, image processing. Sharded by user ID hash. Now moving parts to Kafka for higher throughput.

Discord

SQS + Kafka for message ingest

Discord uses SQS for user-triggered async actions (thumbnail generation, permission updates) and Kafka for message events (fanout to Push + Search + Analytics). Two shapes of "async" problems.

Slack

SNS→SQS fanout for events

Slack's standard: publish events to SNS, fan out to SQS queues per consumer. Adding a new feature = subscribe a new SQS to SNS. Zero producer changes. Every backend team owns their own queue.

Robinhood

SQS FIFO for trade execution

Robinhood uses SQS FIFO for trade orders — strict ordering per user, exactly-once semantics. Each user is a message group. Trades within a group execute in submitted order.

Cloudflare

Custom queue on top of Workers KV + Durable Objects

Cloudflare's Queues product runs at 300+ edge PoPs using Durable Objects for state. Handles millions of msg/sec at edge. Sub-100ms delivery. Powers customers building on Workers.

NATS at Cloud Foundry

Ultra-low-latency internal

Pivotal's Cloud Foundry used NATS for internal control plane messaging — sub-ms delivery between hundreds of internal services. Pattern: NATS core (fire-forget) for speed, JetStream (persistent) for durability.

OpenAI Batch API

The AI-era job queue

OpenAI's Batch API is a modern queue: submit prompts async, get results within 24h, 50% cheaper than sync. Under the hood: a job queue with prioritization, retries, and result storage. Powers embedding pipelines, dataset labeling, RAG indexing.

Key takeaways

  • 1A message queue is a contract about failure — retries, redelivery, at-least-once, dead-letter queue. Build with that contract in mind.
  • 2Distinguish queue (point-to-point) from pub/sub (topic fan-out). Most modern stacks use SNS→SQS to combine both.
  • 3At-least-once + idempotent workers is the correct default. Exactly-once is real but limited in scope and adds complexity.
  • 4Visibility timeout is the mechanism that turns crash-recovery into a solved problem. Set it > expected task time. Consumers must be idempotent.
  • 5DLQ + exponential backoff handles poison messages without blocking the queue. Alert on DLQ growth; drain periodically.
  • 6Consumer groups = horizontal scale for free. Add workers to increase throughput. Kafka partitions cap that scale; plan them.
  • 7Use a queue for tasks, a log for events. Same "messaging" category, different problem shapes.
  • 8For most workloads, managed queues (SQS, Google Pub/Sub, Azure Service Bus) remove 80% of operational pain and cost less than self-hosted RabbitMQ/Kafka.

References & further reading

  • Hohpe, G. & Woolf, B. (2003). Enterprise Integration Patterns. Addison-Wesley. Foundational catalog of messaging patterns.
  • Kreps, J. (2014). "I Heart Logs." O'Reilly. Why the log is the primitive of distributed data.
  • AWS Docs: "How the Amazon SQS Message Lifecycle Works." The definitive reference on visibility timeout + at-least-once.
  • RabbitMQ Docs: "Reliability Guide." Best writeup on acks, confirms, and idempotency.
  • Kafka Improvement Proposals: KIP-98 (exactly-once semantics). How Kafka transactions actually work.
  • Ellis, J. (2016). "Exactly-once Support in Apache Kafka." Confluent blog. Excellent deep-dive.
  • Sean T. Allen (2018). "Message Broker Comparison Guide." Detailed head-to-head of RabbitMQ, Kafka, NATS, Pulsar.
  • Aphyr (Kyle Kingsbury) Jepsen posts: Rigorous consistency tests of every major broker. Read before trusting exactly-once claims.
  • Google Pub/Sub docs: "Message delivery semantics." Best modern managed-pubsub explanation.
  • Kleppmann, M. (2017). DDIA, Chapter 11 (Stream Processing) covers queues + logs together.