The ZMQ_DONTWAIT flag in ZeroMQ doesn’t actually make your send or receive operations non-blocking; it makes them fail immediately if they would have blocked.

Let’s see this in action. Imagine we have a simple PUSH/PULL pattern. The PULL socket is going to be our slow consumer.

import zmq
import time

context = zmq.Context()
pull_socket = context.socket(zmq.PULL)
pull_socket.bind("tcp://127.0.0.1:5555")

push_socket = context.socket(zmq.PUSH)
push_socket.connect("tcp://127.0.0.1:5555")

print("Starting PUSH/PULL with DONTWAIT...")

# Give sockets time to connect
time.sleep(1)

# Send a message without DONTWAIT first
print("Sending first message (blocking)...")
push_socket.send_string("Hello, blocking!")
print("First message sent.")

# Now, try to send a message with DONTWAIT when the PULL socket is busy
print("Attempting to send second message (DONTWAIT)...")
try:
    push_socket.send_string("Hello, DONTWAIT!", zmq.DONTWAIT)
    print("Second message sent (this shouldn't happen if PULL is busy).")
except zmq.Again:
    print("Second message failed to send with DONTWAIT (as expected).")

# Simulate the PULL socket being busy
print("PULL socket is now receiving and processing...")
message = pull_socket.recv_string()
print(f"PULL received: {message}")
time.sleep(2) # Simulate work

print("PULL socket finished processing.")

# Now, try sending with DONTWAIT again after the PULL socket is free
print("Attempting to send third message (DONTWAIT) after PULL is free...")
try:
    push_socket.send_string("Hello, DONTWAIT again!", zmq.DONTWAIT)
    print("Third message sent successfully with DONTWAIT.")
except zmq.Again:
    print("Third message failed to send with DONTWAIT (this shouldn't happen).")

# Let's receive the third message
print("PULL socket receiving third message...")
message = pull_socket.recv_string()
print(f"PULL received: {message}")

push_socket.close()
pull_socket.close()
context.term()

When you run this, you’ll see the first message send fine. Then, the second message, sent with ZMQ_DONTWAIT, will immediately raise a zmq.Again exception because the PULL socket is not yet ready to receive (it’s just been bound and hasn’t processed the first message yet). After the PULL socket processes the first message and takes a short time.sleep(2), the third message, also sent with ZMQ_DONTWAIT, will go through successfully because the PULL socket is now ready.

The core problem ZMQ_DONTWAIT addresses is that ZeroMQ’s send and receive operations, by default, will block if the underlying transport buffer is full (for sending) or empty (for receiving). This means your thread will pause execution, waiting for an I/O event. This is often desirable for simplicity, but in high-performance or event-driven architectures, you want to avoid blocking. ZMQ_DONTWAIT lets you poll the socket: if the operation would block, it returns immediately with a zmq.Again error instead of waiting. This allows your application to continue processing other tasks, check the socket again later, or handle the condition gracefully.

The ZMQ_DONTWAIT flag is applied as an option during the send or recv call itself, not to the socket. For example: socket.send(message, flags=zmq.DONTWAIT). The corresponding error you’ll catch is zmq.Again.

The internal mechanism involves the ZeroMQ kernel checking the state of the socket’s internal queues and the underlying OS buffers. If a send operation would cause the queue to exceed its high-water mark, or if a receive operation finds no messages available, instead of yielding control and waiting for the OS to signal readiness, it immediately returns the zmq.Again error. This is a form of busy-waiting or polling at the application level, managed by the ZeroMQ library.

A common misconception is that ZMQ_DONTWAIT makes the entire socket non-blocking. It only affects the specific send or recv call it’s used with. Other operations on the same socket remain blocking unless ZMQ_DONTWAIT is also applied to them.

The other "non-blocking" mechanism in ZeroMQ is actually using zmq.poll(). While ZMQ_DONTWAIT tells ZeroMQ "try once and tell me if it would block," zmq.poll() tells ZeroMQ "wait for a specific amount of time (or indefinitely) until a socket is ready for a specific operation (read, write, etc.), and then tell me." zmq.poll() is the more robust way to handle non-blocking I/O as it doesn’t involve busy-waiting in your application code.

You’ll often see ZMQ_DONTWAIT used in conjunction with zmq.poll(). A common pattern is to poll() for readiness and then, if the socket is ready, attempt a send or recv with ZMQ_DONTWAIT. This might seem redundant, but it can be useful in scenarios where you have multiple operations pending on a single socket and want to prioritize or handle them in a specific order without blocking.

If you fix all your ZMQ_DONTWAIT related zmq.Again errors by ensuring your sockets are ready, the next error you’ll likely encounter is related to message framing or deserialization if you’re not consistently sending and receiving the same data types or structures.

Want structured learning?

Take the full Zeromq course →