System Design Fundamentals · Distributed Systems Building Blocks
Message Queues and Asynchronous Processing
Not every piece of work needs to happen inside the request/response cycle. A message queue lets a service hand off work to be processed later, by a different process, without the original caller waiting for it to finish.
Producer Queue Consumer(s)
┌──────────┐ publish ┌───────┐ consume ┌────────────┐
│ API │ ──────────→│ Queue │ ────────────→│ Worker pool│
│ server │ └───────┘ └────────────┘
└──────────┘
responds to user
immediately, work
happens async
Why this matters
- Decoupling — the producer doesn't need to know anything about how the work gets done, or by what, or how many workers exist.
- Load leveling — a sudden burst of 10,000 requests becomes a queue with 10,000 items, processed at whatever steady rate the workers can sustain, instead of 10,000 requests slamming a downstream service simultaneously.
- Retry-ability — if a worker crashes mid-task, an unacknowledged message can be redelivered instead of the work being silently lost.
When to reach for one
Good candidates for async processing: sending emails, generating a thumbnail after an image upload, updating a search index, running a report. Bad candidates: anything the user is actively waiting for a correct, immediate answer to (an API that returns "was my payment approved?" shouldn't be async, even though "send the receipt email" absolutely should be).
At-least-once vs. exactly-once delivery
Most real queues guarantee at-least-once delivery, not exactly-once — a message might be delivered twice (e.g. a worker processes it, then crashes before acknowledging). This means consumers need to be idempotent: processing the same message twice must produce the same result as processing it once (e.g. "set the user's status to verified," not "increment a counter by one").