Skip to content

What Is a Message Queue?

The problem with calling services directly

Section titled “The problem with calling services directly”

Imagine an order service that, when an order is placed, must: charge a card, send a confirmation email, update inventory, and notify the warehouse. The obvious approach is to call each service directly and wait for each to finish.

That works until it doesn’t:

  • The email service is slow. Now placing an order is slow, because you’re blocked waiting on email.
  • The warehouse service is down. Now the whole order fails — even though the payment succeeded.
  • A flash sale hits. Ten thousand orders per second slam every downstream service at once, and the slowest one falls over.

The root problem: direct calls couple the caller’s fate to the callee’s. If the callee is slow, you’re slow. If it’s down, you fail. If you spike, it spikes.

A message queue puts a buffer in the middle. The order service publishes a message (“order 123 placed”) and immediately returns. Each downstream service consumes those messages on its own schedule.

flowchart TB
  subgraph direct["Direct calls — coupled fate"]
    o1["order service"] -->|blocks| e1["email (slow)"]
    o1 -->|fails if down| w1["warehouse"]
  end
  subgraph queued["Via a queue — decoupled"]
    o2["order service"] -->|publish, return| q["queue (buffers)"]
    q --> e2["email consumer
(own pace)"]
    q --> w2["warehouse consumer
(catches up later)"]
  end
Direct coupling versus a buffering queue

Three things fall out of that one change:

  1. Decoupling. The producer doesn’t know or care who consumes, how many consumers there are, or whether they’re up right now.
  2. Buffering (load leveling). A spike of 10,000 messages sits safely in the queue; consumers drain it at a steady rate instead of being crushed.
  3. Resilience. If a consumer is down, messages wait in the queue. When it comes back, it catches up — nothing is lost.

The trade-off: you gain async, you lose “immediately”

Section titled “The trade-off: you gain async, you lose “immediately””

Queues are not free. In exchange for decoupling you accept:

  • Eventual, not immediate. The email is sent soon, not before the HTTP response returns. Your UX and data model must tolerate that gap.
  • More moving parts. The broker is now critical infrastructure you must run, monitor, and secure.
  • Harder debugging. A request no longer has one linear stack trace; it fans out across producers, queues, and consumers.

Reach for a message queue when:

  • Work can happen asynchronously — the caller doesn’t need the result to respond.
  • You need to absorb spikes or smooth out bursty load.
  • Services should be decoupled so one failing doesn’t cascade.
  • You want to fan work out to many workers or broadcast an event to many subscribers.

Stay synchronous (plain HTTP/gRPC) when:

  • The caller needs the answer now to continue (e.g. “is this username taken?”).
  • The interaction is a simple, fast request/response with no spikes.
  • Adding a broker’s operational weight isn’t worth it for the problem at hand.
What is the root problem with services calling each other directly?
Which three benefits does adding a queue provide?
What do you give up by moving work behind a queue?
Which task is a poor fit for a message queue?