ZeroMQ’s SUBSCRIBE socket option is actually a prefix-matching glob.
Let’s see it in action. We’ll set up a simple publisher and subscriber.
Publisher (pub.py):
import zmq
import time
context = zmq.Context()
socket = context.socket(zmq.PUB)
socket.bind("tcp://*:5556")
print("Publisher started on tcp://*:5556")
for i in range(100):
message = f"USER.alice.message.{i}"
print(f"Sending: {message}")
socket.send_string(message)
time.sleep(0.1)
message = f"SYSTEM.log.error.{i}"
print(f"Sending: {message}")
socket.send_string(message)
time.sleep(0.1)
Subscriber (sub_prefix.py):
import zmq
context = zmq.Context()
socket = context.socket(zmq.SUB)
socket.connect("tcp://localhost:5556")
# Subscribe to messages starting with "USER."
socket.setsockopt_string(zmq.SUBSCRIBE, "USER.")
print("Subscriber started, subscribing to 'USER.' prefixes.")
while True:
message = socket.recv_string()
print(f"Received: {message}")
If you run pub.py and then sub_prefix.py, you’ll see the subscriber only prints messages that begin with USER.. The SYSTEM.log.error.* messages are silently dropped by the ZeroMQ library on the subscriber side before they even reach your application code.
This works because the SUBSCRIBE option doesn’t just filter by exact string match; it’s a glob pattern. The asterisk (*) is not a special character here. Any string you pass to setsockopt_string(zmq.SUBSCRIBE, ...) is treated as a literal prefix. The ZeroMQ library on the subscriber side will only deliver messages whose first byte matches the first byte of the subscription string, the second byte matches the second byte, and so on, up to the length of the subscription string.
The problem this solves is efficient message routing in a many-to-many Pub/Sub scenario. Without this, the publisher would have to send every message to every subscriber, and the subscribers would then have to filter them in their application code. This would be incredibly inefficient, especially with many subscribers and high message volumes. ZeroMQ handles the filtering at the transport level, reducing network traffic and CPU load on the subscribers.
The exact levers you control are the strings you pass to setsockopt_string(zmq.SUBSCRIBE, ...). You can have multiple SUBSCRIBE options set on a single socket. If a message matches any of the subscribed prefixes, it will be delivered.
For example, if you wanted to receive both USER. and SYSTEM. messages, you’d do this in the subscriber:
socket.setsockopt_string(zmq.SUBSCRIBE, "USER.")
socket.setsockopt_string(zmq.SUBSCRIBE, "SYSTEM.")
The most surprising thing about this mechanism is how simple and yet how powerful it is. It feels like it should be more complex, involving wildcards or regex, but it’s just a direct, byte-by-byte prefix match. This simplicity makes it incredibly fast and predictable. The ZeroMQ library on the subscriber side maintains an internal list of all subscribed prefixes. When a message arrives, it iterates through this list, performing a startswith() check (or equivalent byte comparison) for each prefix against the incoming message. If any prefix matches, the message is accepted.
The next concept you’ll likely explore is how to handle more complex filtering, such as subscribing to specific "topics" that aren’t simple prefixes, which often leads to using a routing layer or a dedicated message queue.