Skip to content

Request-Reply Pattern — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Request. We cover key concepts, practical examples, and best practices to help you master this topic.

The request-reply pattern enables synchronous-style request-response communication over asynchronous message queues using correlation IDs and temporary reply queues.

What You'll Learn

By the end of this lesson, you will understand the request-reply pattern, how to implement it with RabbitMQ, how correlation IDs match requests to responses, and when to use this pattern over direct HTTP calls.

Why It Matters

Some operations need a response: "Is this user valid?", "What is the exchange rate?", "Process this payment and give me the receipt." Direct HTTP calls work but couple the services. The request-reply pattern over a message queue provides decoupling with response correlation.

Real-World Use

A fraud detection service needs to check a Transaction before processing. The payment service sends a request message with order details and waits for a response. The fraud service processes it and replies with a risk score. Both services are decoupled by the queue.

Request-Reply Architecture

sequenceDiagram
    participant C as Client
    participant Q as Request Queue
    participant S as Server
    participant R as Reply Queue

    C->>Q: Send request (correlation_id, reply_to)
    Q->>S: Deliver request
    S->>C: Send reply (correlation_id)
    C->>C: Match reply by correlation_id

The client sends a request with a unique correlation ID and specifies a reply queue. The server processes the request and sends the response to the reply queue with the same correlation ID. The client matches responses by correlation ID.

Implementing Request-Reply with RabbitMQ

import pika
import json
import uuid

class RPCClient:
    def __init__(self):
        self.connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
        self.channel = self.connection.channel()
        result = self.channel.queue_declare(queue='', exclusive=True)
        self.callback_queue = result.method.queue
        self.channel.basic_consume(
            queue=self.callback_queue,
            on_message_callback=self.on_response,
            auto_ack=True
        )
        self.response = None
        self.correlation_id = None

    def on_response(self, ch, method, properties, body):
        if self.correlation_id == properties.correlation_id:
            self.response = body

    def call(self, request_data):
        self.correlation_id = str(uuid.uuid4())
        self.response = None

        self.channel.basic_publish(
            exchange='',
            routing_key='rpc_queue',
            body=json.dumps(request_data),
            properties=pika.BasicProperties(
                reply_to=self.callback_queue,
                correlation_id=self.correlation_id,
            )
        )

        while self.response is None:
            self.connection.process_data_events()
        return json.loads(self.response)

client = RPCClient()
result = client.call({'method': 'add', 'args': [5, 3]})
print(f"Result: {result}")

Expected output:

Result: 8
# RPC Server
import pika
import json

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='rpc_queue')

def on_request(ch, method, properties, body):
    request = json.loads(body)
    print(f"Received: {request}")

    if request['method'] == 'add':
        result = sum(request['args'])
    else:
        result = None

    ch.basic_publish(
        exchange='',
        routing_key=properties.reply_to,
        body=json.dumps(result),
        properties=pika.BasicProperties(correlation_id=properties.correlation_id)
    )
    ch.basic_ack(delivery_tag=method.delivery_tag)

channel.basic_qos(prefetch_count=1)
channel.basic_consume(queue='rpc_queue', on_message_callback=on_request)
print("RPC Server waiting for requests...")
channel.start_consuming()

Expected output:

RPC Server waiting for requests...
Received: {'method': 'add', 'args': [5, 3]}

Why Correlation IDs Matter

Think of a busy restaurant kitchen. Multiple waiters place orders on the same rack. When a dish is ready, the chef calls out the table number. Each waiter knows which table ordered what dish.

Correlation IDs work the same way. Multiple clients send requests. The server processes them in any order and sends each response with the original correlation ID. Each client listens for its own correlation ID and ignores responses meant for others.

Request-Reply vs HTTP

Aspect Request-Reply (MQ) HTTP
Coupling Decoupled Direct connection
Timeout Client manages Built-in (timeout)
Load leveling Queue buffers No buffering
Response guarantee At-least-once Best-effort
Service discovery Queue name URL + DNS
Latency Higher Lower

Common Mistakes

1. Forgetting the Reply Queue

The request must specify a reply_to queue. Without it, the server has no way to send the response. The client creates an exclusive queue for replies.

2. Ignoring Timeout

A request-reply client can block forever if the server crashes or the message is lost. Always set a timeout. If no response arrives within the timeout, the client should fail gracefully.

3. Using the Same Correlation ID for Multiple Requests

Each request must have a unique correlation ID. Reusing IDs causes the wrong response to be matched to a request. Use UUIDs or incrementing counters.

4. Blocking the Client Event Loop

The RPC client pattern blocks the event loop waiting for a response. In production, use asynchronous RPC or set a timeout. Blocking the event loop prevents the client from handling other tasks.

5. Not Handling Server Failures

If the server crashes after processing but before sending the response, the client never gets a response. Implement server-side idempotency and client-side retry with timeout.

Practice Questions

1. How does the request-reply pattern work over message queues?

The client sends a request with a correlation ID and a reply queue address. The server processes it and sends the response to the reply queue with the same correlation ID. The client matches responses by correlation ID.

2. What is a correlation ID?

A unique identifier that pairs a request with its response. The client generates it and includes it in the request. The server echoes it back in the response so the client knows which request the response belongs to.

3. When would you use request-reply over HTTP?

When you want the decoupling benefits of message queues (load leveling, fault tolerance, at-least-once delivery) while still needing a response. Use HTTP when simplicity and lower latency are more important.

4. How do you handle timeout in request-reply?

Set a timeout on the client side. If no response arrives within the timeout, the client can retry the request or fail. Use a message TTL to prevent abandoned requests from accumulating.

Challenge

Design a distributed calculator service using request-reply. Clients send calculation requests (add, multiply, sqrt). Multiple server instances handle requests. Implement timeout, retry, and Load Balancing across servers.

FAQ

Does request-reply work with Kafka?

Not directly. Kafka is pull-based and not designed for RPC. Use Kafka for event streaming and RabbitMQ or HTTP for request-reply patterns.

Can request-reply scale horizontally?

Yes. Multiple server instances consume from the same request queue. Multiple clients send requests. Correlation IDs ensure responses reach the correct client.

What happens to abandoned requests?

Requests with no matching reply (server crashed) remain in queues. Use message TTL to auto-expire abandoned requests. The client timeout should be shorter than the TTL.

Is request-reply slower than HTTP?

Yes. Each request-reply round trip goes through queue enqueue, dequeue, and reply. Expect 2-5x higher latency compared to direct HTTP calls.

Should I use request-reply internally?

Use it when you need decoupling and async processing with a response. For simple internal calls where services are always available, HTTP is simpler.

Mini Project: RPC Calculator

import pika
import json
import uuid

class CalculatorServer:
    def __init__(self):
        self.conn = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
        self.ch = self.conn.channel()
        self.ch.queue_declare(queue='calc_requests')
        self.ch.basic_consume(queue='calc_requests', on_message_callback=self.handle)

    def handle(self, ch, method, properties, body):
        req = json.loads(body)
        op = req['op']
        a, b = req['a'], req['b']

        if op == 'add': result = a + b
        elif op == 'sub': result = a - b
        elif op == 'mul': result = a * b
        elif op == 'div': result = a / b if b != 0 else 'error'

        ch.basic_publish(
            exchange='', routing_key=properties.reply_to,
            body=json.dumps(result),
            properties=pika.BasicProperties(correlation_id=properties.correlation_id)
        )
        ch.basic_ack(delivery_tag=method.delivery_tag)
        print(f"Calculated: {a} {op} {b} = {result}")

    def start(self):
        self.ch.start_consuming()

import threading
server = CalculatorServer()
t = threading.Thread(target=server.start, daemon=True)
t.start()

Expected output:

Calculated: 5 + 3 = 8

What's Next

Now that you understand request-reply, explore competing consumers for distributing work across multiple workers, then learn about fanout exchanges for broadcast messaging.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro