ZeroMQ’s multipart messages aren’t just "multiple messages sent together"; they’re a single, atomic unit composed of distinct, ordered "frames."

Let’s watch it in action. Imagine a simple request-reply scenario. We’ll send a command ("GET") followed by its parameter ("users/123") as two separate frames in one multipart message. The server receives this as a single unit, processes the "GET" and "users/123" frames, and sends back a reply, also as a multipart message: a status code ("200") and the data ("Alice").

Sender (Python):

import zmq

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

print("Sending 'GET users/123'...")
socket.send_string("GET", zmq.SNDMORE)  # Send the first frame, more to come
socket.send_string("users/123")      # Send the second frame, last one

message = socket.recv_multipart()
print(f"Received reply: {message}")

socket.close()
context.term()

Receiver (Python):

import zmq

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

print("Waiting for request...")
message = socket.recv_multipart()
print(f"Received message: {message}")

command = message[0].decode('utf-8')
param = message[1].decode('utf-8')

if command == "GET" and param == "users/123":
    print("Processing request...")
    socket.send_string("200", zmq.SNDMORE)
    socket.send_string("Alice")
    print("Sent reply '200 Alice'")
else:
    socket.send_string("404", zmq.SNDMORE)
    socket.send_string("Not Found")
    print("Sent reply '404 Not Found'")

socket.close()
context.term()

When you run the sender and then the receiver, you’ll see the output showing the frames being sent and received as a single logical operation. The recv_multipart() call on both sides collects all frames belonging to that atomic message.

The core problem multipart messages solve is enabling complex, structured communication patterns over ZeroMQ’s otherwise simple socket APIs. Think of it like building a custom protocol on top of a simple pipe. Instead of serializing a whole object into a single byte string (which requires careful de-serialization on the other end), you can send distinct pieces of data independently, yet atomically. This is incredibly useful for things like:

  • Command/Argument pairs: As shown above, sending a command and its parameters.
  • Request/Response with metadata: A request might have an identity frame, a command frame, and a payload frame. The response could have a status frame, a timestamp frame, and the actual data frame.
  • Routing with envelopes: A message can carry its own routing information as leading frames. For example, a router socket might receive a multipart message where the first frame is the client’s identity, and the second is the actual message payload. The router then uses the identity frame to send the response back to the correct client.

Internally, ZeroMQ treats a multipart message as a single unit of work. When you call send_string("frame1", zmq.SNDMORE) followed by send_string("frame2"), ZeroMQ buffers these frames. Only when the last frame is sent does ZeroMQ actually transmit the entire message over the network. Similarly, recv_multipart() will block until all frames of a single logical message have been received. This atomicity guarantees that you either get the whole message, or none of it, preventing partial data corruption or inconsistent states.

The zmq.SNDMORE flag is the key. It tells ZeroMQ, "This is not the last frame of this message; expect more." Without it on all but the final frame, ZeroMQ would treat each send() as a separate, complete message. The recv_multipart() method, when called, will consume all frames associated with the current logical message. If you call recv_multipart() again without sending a new message, it will block indefinitely or raise an error, depending on socket options.

Most people understand the SNDMORE flag for sending. What’s less obvious is how it affects receiving and how ZeroMQ internally manages the buffering and delivery. When you call recv_multipart(), ZeroMQ doesn’t just grab one frame; it looks at the underlying transport. If it sees that the next incoming data belongs to the same logical message (because the sender used SNDMORE), it continues to buffer those frames until it detects the end of the message. This internal buffering and state management is what gives you the atomic delivery guarantee. It’s not just about sending multiple things; it’s about sending a single, structured entity.

The next logical step after mastering multipart messages is understanding how to use them for more complex routing patterns, particularly with ROUTER/DEALER socket pairs and message queuing.

Want structured learning?

Take the full Zeromq course →