The most surprising thing about ZeroMQ’s DEALER-ROUTER pattern is that it’s not actually a direct replacement for synchronous request-reply; it’s an asynchronous, fully decoupled, message-queue-like system that happens to implement a request-reply communication flow.

Let’s see it in action. Imagine a scenario where you have a pool of workers ready to process tasks, and multiple clients sending those tasks.

Server Side (ROUTER):

import zmq
import time

context = zmq.Context()
socket = context.socket(zmq.ROUTER)
socket.bind("tcp://*:5555")

print("ROUTER started. Waiting for messages...")

while True:
    try:
        # ROUTER receives sender's identity and the message
        identity, message = socket.recv_multipart()
        print(f"Received from {identity.decode()}: {message.decode()}")

        # Simulate work
        time.sleep(1)

        # Send reply back to the specific identity
        reply = f"Processed: {message.decode()}".encode()
        socket.send_multipart([identity, reply])
        print(f"Sent reply to {identity.decode()}")

    except KeyboardInterrupt:
        print("Shutting down ROUTER...")
        break

socket.close()
context.term()

Client Side (DEALER):

import zmq
import random
import time

context = zmq.Context()
socket = context.socket(zmq.DEALER)
socket.connect("tcp://localhost:5555")

client_id = f"CLIENT-{random.randint(1000, 9999)}".encode()
print(f"DEALER started as {client_id.decode()}. Sending requests...")

for i in range(5):
    request = f"Task-{i}".encode()
    print(f"Sending: {request.decode()}")
    socket.send(request)

    # In a real async app, you'd do other work here or have a separate receive loop
    # For demonstration, we'll receive immediately
    reply = socket.recv()
    print(f"Received reply: {reply.decode()}")
    time.sleep(0.5) # Simulate some delay between requests

print("Finished sending requests.")
socket.close()
context.term()

When you run these, you’ll notice that the ROUTER receives messages with the sender’s identity attached, and it must use that identity to send replies back. The DEALER, on the other hand, just sends messages and receives replies without needing to manage identities itself. ZeroMQ handles the routing of replies back to the correct DEALER.

The core problem DEALER-ROUTER solves is managing a pool of workers that can process requests asynchronously. A traditional REQ-REP socket is synchronous: a REQ socket must send a request and then must wait for a reply before it can send another. If the REP socket is busy, the REQ socket blocks.

With DEALER-ROUTER, the ROUTER acts like a smart message broker. It can distribute incoming requests from multiple DEALERs to a pool of worker sockets (which could also be DEALERs, or even PULL sockets if they don’t need to send replies). Crucially, the ROUTER remembers which DEALER sent which message and ensures the reply goes back to the correct one. DEALER sockets, conversely, can send multiple requests without waiting for replies and can receive replies out of order, or even receive replies to requests they sent much earlier. This is the asynchronous nature: the sending and receiving are decoupled.

The internal mechanism of ROUTER is that it maintains an internal queue of pending requests and a mapping of client identities to their sockets. When a request arrives, it picks an available worker and forwards the request along with the client’s identity. When a worker finishes, it sends the reply back to the ROUTER, which then uses the stored identity to send it back to the original DEALER. If a DEALER sends a message and the ROUTER has no available workers, the message simply waits in the ROUTER’s internal queue until a worker becomes free.

A key aspect often missed is how the DEALER socket handles multiple outstanding requests. A single DEALER socket can send multiple messages without receiving replies. When replies come back, they are delivered to the DEALER socket in the order they are processed by the ROUTER and its workers. The DEALER socket itself doesn’t inherently know which reply belongs to which sent request unless you add correlation IDs in your message payloads. The DEALER’s recv() call will simply return the next available reply from the ROUTER. This means your application logic on the DEALER side needs to be prepared to match replies to requests, perhaps by including a unique ID in each message sent.

The next logical step after mastering DEALER-ROUTER for async request-reply is exploring how to implement resilient worker pools using ZMQ_ROUTER’s zmq_setsockopt(ZMQ_ROUTER_MANDATORY, 1) to ensure requests are only sent if a worker is available.

Want structured learning?

Take the full Zeromq course →