Request-Reply Pattern
Synchronous communication over messaging: correlation IDs, reply queues, timeouts, and when to use request-reply vs fire-and-forget.
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
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.
// 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:
| Strategy | Description | Pros | Cons |
|---|---|---|---|
| Per-request temporary queue | Create a new ephemeral queue per request; delete after reply | Clean isolation; no routing logic needed | Queue creation overhead per request; not suitable for high throughput |
| Shared reply queue per service | One persistent reply queue per service instance; use correlation ID to route internally | Low overhead; reusable | All 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
| Aspect | Request-Reply | Fire-and-Forget |
|---|---|---|
| Coupling | Temporal coupling (requester waits) | Fully decoupled |
| Use case | Caller needs the result to proceed | Caller doesn't need or can't wait for result |
| Complexity | Higher (correlation ID, reply queue, timeout) | Lower |
| Failure handling | Caller knows immediately about failures (via timeout) | Caller doesn't know if processing succeeded |
| Examples | Payment authorization, order validation | Email 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.