Overview
A message queue is a buffer between services: producers put messages on the queue, and consumers take them off and process them, at their own pace. This decouples the two sides in time (the consumer can be down or slow), in scale (add consumers independently), and in failure (a crash does not lose work that is still in the queue).
Queues turn synchronous chains into asynchronous workflows. Instead of making the user wait for an email to be sent and a PDF to be generated, the API enqueues jobs and returns immediately. Each message is normally processed by one consumer, and it is removed only after the consumer acknowledges it, so failed work is retried.
Waiters clip order tickets on a rail; cooks take the next ticket when ready. Waiters do not wait for food to be cooked before taking more orders, and during a rush, tickets simply pile up until cooks catch up.
When to use it
- Slow or unreliable work that should not block user requests (emails, images, reports).
- Absorbing traffic spikes (load leveling).
- Decoupling services so one being down does not break another.
- Distributing work across a pool of workers.
Where it shows up in interviews
Recognize it when: the request triggers slow work.
- Design a video upload pipeline
- Design an email notification system
Recognize it when: traffic spikes exceed downstream capacity.
- Design a flash sale system
- Design a ticketing queue
Where it is used in real software
Fully managed queue used for decoupling microservices; standard queues scale nearly without limit.
Background job frameworks on top of Redis or RabbitMQ for Ruby, Python, and Node.
Use queues and streams for payments, dispatch, and order processing to absorb peak load.
Key terms
- Producer / consumer
- Sends messages / receives and processes them.
- Acknowledgment
- Consumer confirms processing so the message is deleted.
- Visibility timeout
- Message hidden from others while one consumer works on it.
- Backlog / queue depth
- Number of messages waiting.
- Competing consumers
- Many workers share one queue; each message goes to one.
How it works, step by step
- 1Producer sends a message
Small payload with IDs, not large blobs.
- 2Queue stores it durably
Replicated so broker failures do not lose it.
- 3Consumer receives it
Message becomes invisible to other consumers.
- 4Consumer processes and acks
The queue deletes the message.
- 5Failures are retried
No ack before timeout: redelivered; too many failures: dead-letter queue.
STEP 1The user uploads a video. The API stores the file and creates a job.
Synchronous vs queued signup
Signup sends a welcome email, creates a CRM record, and resizes an avatar
| Aspect | Synchronous | With a queue |
|---|---|---|
| User-facing latency | ~2.5 s (sum of all steps) | ~100 ms (save user + enqueue) |
| Email provider down | Signup fails | Emails retried later |
| Traffic spike | Everything slows | Backlog grows, workers catch up |
| Scaling | Scale all together | Scale workers independently |
NOWAspect: User-facing latency | Synchronous: ~2.5 s (sum of all steps) | With a queue: ~100 ms (save user + enqueue)
Queues trade immediate completion for resilience and responsiveness; the UI must handle 'still processing' states.
Implementation
import { SQSClient, SendMessageCommand, ReceiveMessageCommand, DeleteMessageCommand } from "@aws-sdk/client-sqs"; const sqs = new SQSClient({});const QueueUrl = process.env.EMAIL_QUEUE_URL!; // Producerawait sqs.send(new SendMessageCommand({ QueueUrl, MessageBody: JSON.stringify({ type: "welcome", userId: "42" }) })); // Consumer loopwhile (true) { const { Messages = [] } = await sqs.send(new ReceiveMessageCommand({ QueueUrl, MaxNumberOfMessages: 10, WaitTimeSeconds: 20, VisibilityTimeout: 60, // long polling })); for (const msg of Messages) { try { await sendWelcomeEmail(JSON.parse(msg.Body!)); // must be idempotent await sqs.send(new DeleteMessageCommand({ QueueUrl, ReceiptHandle: msg.ReceiptHandle! })); } catch (err) { console.error("will be retried after visibility timeout", err); } }}Complexity and performance
Milliseconds.
Little's Law.
Trade-offs
Users get fast responses but results arrive later; design status endpoints or notifications.
Asynchronous flows are harder to trace and debug; add correlation IDs and tracing.
Variants and related techniques
Separate queues for urgent and bulk work.
Messages become visible after a delay, for scheduled retries.
Ordered, exactly-once processing within a group, at lower throughput.
Common mistakes
- Non-idempotent consumers.
Fix: Messages can be delivered more than once; deduplicate.
- Huge messages.
Fix: Store payloads in object storage and send a reference.
- No monitoring of queue age.
Fix: Alert on the age of the oldest message, not just depth.
Interview questions
Why put a queue between the API and workers?
It decouples request handling from slow work, lets the API respond quickly, absorbs traffic spikes, allows independent scaling of workers, and retries failed work without losing it.
How do you size the worker pool?
Workers = arrival rate x average processing time (Little's Law), plus headroom; auto scale on queue depth or oldest-message age.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Move email sending to a queue | Easy | Async processing. |
| Design a video transcoding pipeline | Medium | Queues and workers. |