Menu

Request-Reply Pattern

Synchronous communication over messaging: correlation IDs, reply queues, timeouts, and when to use request-reply vs fire-and-forget.

10 min read

Why Request-Reply Over Messaging?

Most inter-service communication in microservices is synchronous: service A calls service B and waits for an answer. Normally this is done with HTTP/gRPC. But sometimes you want to route these request-response interactions through a message broker — so you get broker benefits (durability, routing, retries) while still maintaining the call-and-response semantics the caller needs.

The Request-Reply pattern achieves this by having the requester publish a message to a request queue and wait for a response message on a dedicated reply queue. A correlation ID ties the response back to the original request. This is sometimes called the RPC over messaging pattern.

Pattern Flow

Loading diagram...
Request-Reply over messaging: correlation IDs link responses to their original requests

Correlation ID

The correlation ID is a UUID (or similar unique token) generated by the requester when sending a message. It is echoed back verbatim by the responder in its reply. The requester maintains a map of `correlationId → pending promise/callback` and uses the correlation ID from the incoming reply to resolve the right waiter.

typescript
// Requester side — simplified
const pending = new Map<string, (result: unknown) => void>();

async function sendRequest(payload: unknown): Promise<unknown> {
  const correlationId = crypto.randomUUID();
  const replyTo = "replies.service-a." + correlationId;

  return new Promise((resolve) => {
    pending.set(correlationId, resolve);

    broker.publish("requests.service-b", {
      correlationId,
      replyTo,
      payload,
    });

    // Timeout guard — don't wait forever
    setTimeout(() => {
      if (pending.has(correlationId)) {
        pending.delete(correlationId);
        resolve({ error: "timeout" });
      }
    }, 5000);
  });
}

// On incoming reply from reply queue:
broker.subscribe(myReplyQueue, (msg) => {
  const resolve = pending.get(msg.correlationId);
  if (resolve) {
    pending.delete(msg.correlationId);
    resolve(msg.payload);
  }
});

Reply Queue Strategies

There are two common strategies for reply queues:

StrategyDescriptionProsCons
Per-request temporary queueCreate a new ephemeral queue per request; delete after replyClean isolation; no routing logic neededQueue creation overhead per request; not suitable for high throughput
Shared reply queue per serviceOne persistent reply queue per service instance; use correlation ID to route internallyLow overhead; reusableAll replies land in one queue — requires client-side demux by correlation ID
💡

RabbitMQ Direct Reply-To

RabbitMQ has a built-in optimization called `amq.rabbitmq.reply-to`: a pseudo-queue that routes replies directly back to the consuming connection without creating a real queue. This avoids the per-request queue overhead while keeping the pattern clean.

Timeouts Are Non-Negotiable

Unlike HTTP where a closed connection signals failure, a request waiting for a reply on a queue has no inherent signal that the responder died. You must implement a client-side timeout. Without it, your `pending` map will grow unbounded and your callers will block indefinitely. Choose timeouts based on P99 processing latency of the responder plus a safety margin — typically 2–5x the expected response time.

Request-Reply vs Fire-and-Forget

AspectRequest-ReplyFire-and-Forget
CouplingTemporal coupling (requester waits)Fully decoupled
Use caseCaller needs the result to proceedCaller doesn't need or can't wait for result
ComplexityHigher (correlation ID, reply queue, timeout)Lower
Failure handlingCaller knows immediately about failures (via timeout)Caller doesn't know if processing succeeded
ExamplesPayment authorization, order validationEmail notification, audit logging, analytics events
💡

Interview Tip

If an interviewer describes a scenario where 'Service A needs the result of Service B's computation before it can proceed,' that is a request-reply scenario. Ask: can this be made async (async request-reply)? If the operation takes longer than ~100ms, async request-reply with a callback is usually better than blocking the caller with synchronous request-reply over messaging.

📝

Knowledge Check

4 questions

Test your understanding of this lesson. Score 70% or higher to complete.

Ask about this lesson

Ask anything about Request-Reply Pattern