pyzmq review
pyzmq 27.2.0 is the Python binding for ZeroMQ, exposing contexts, sockets, polling, multipart messages, CURVE security, monitoring, and asyncio-compatible sockets. ZeroMQ connects peers directly with patterns such as request/reply, publish/subscribe, push/pull, and router/dealer; pyzmq does not add a broker or durable queue. Version 27.2 expands type coverage, adds python -m zmq.curve_keygen, raises the minimum Python to 3.9, and updates wheels and builds for newer CPython, Cython, and Visual Studio releases.
pyzmq 27.2.0 installed as one 4 MB package in 0.2 seconds, imported in 0.21 seconds, and produced zero pip-audit findings in our sandbox. Use it for direct cross-language messaging when your team will own delivery rules; choose a broker when persistence and redelivery belong in infrastructure.
We installed it
| Install | ✓ · 0.2s | 1 package on disk · 4 MB |
| Import | ✓ | import zmq in 0.21s · compiled extensions · py.typed · requires Python >=3.9 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does pyzmq install cleanly?
Yes. In a fresh container with an empty cache, pip install pyzmq finished in 0.2s, leaving 1 package and 4 MB on disk. pip-audit reported no known vulnerabilities.
What does pyzmq need to run?
Python >=3.9, and a platform wheel with compiled extensions. In our run import zmq succeeded in 0.21s, and the package ships py.typed for type checkers.
pyzmq or pynng: which should you use?
pynng: Choose it for NNG messaging patterns when that C library and its dialer or listener model fit the deployment. pyzmq 27.2.0 installed as one 4 MB package in 0.2 seconds, imported in 0.21 seconds, and produced zero pip-audit findings in our sandbox.
When should you not use pyzmq?
Messages must survive every process restart or be replayed by consumer offset; ZeroMQ has no built-in durable broker or log
Use it if
- Processes you control need low-latency messaging over inproc, IPC, or TCP without operating a separate broker
- The protocol naturally matches PUB/SUB broadcasts, PUSH/PULL work distribution, REQ/REP calls, or ROUTER/DEALER routing
- A Python service needs to communicate with ZeroMQ peers written in C, C++, Rust, Java, or another supported language
- The team can define framing, retries, authentication, queue limits, and shutdown behavior as application protocol
- Messages must survive every process restart or be replayed by consumer offset; ZeroMQ has no built-in durable broker or log
- The application needs broker-managed acknowledgements, redelivery, dead-letter queues, and routing administration; RabbitMQ or NATS fits those operations better
- Developers are likely to share one socket across threads; pyzmq documents contexts as thread-safe and ordinary sockets as unsafe to share
- A source build is unacceptable on an unsupported platform; pyzmq ships compiled extensions and falls back to a CMake-based libzmq build when no matching wheel exists
- Untrusted peers can reach the endpoint but nobody will own authentication and message validation; plain ZeroMQ sockets do not authenticate payload senders
Setup reality
Our pyzmq 27.2.0 install completed in 0.2 seconds in a clean Python 3.12 Bookworm sandbox. One package used 4 MB, pip-audit found zero known vulnerabilities, and import zmq succeeded in 0.21 seconds. The distribution reports 1 direct dependency, requires Python 3.9 or newer, includes compiled .so extensions, and ships py.typed. The lab metadata did not identify a license.
That fast path came from a compatible wheel. Unsupported systems may compile pyzmq and bundled libzmq through CMake, or link against an existing libzmq. Version 27.2 adds a CPython 3.15 free-threaded wheel and stops publishing the CPython 3.13 free-threaded wheel, so free-threaded deployments need exact wheel checks. Windows wheels leave IPC disabled because the project documents crashes in that transport.
A Context may be shared among threads, while each ordinary socket belongs to one thread. Create fresh sockets after a process fork as well. Socket types have state machines: a REQ socket must send before it receives, SUB sees only subscribed prefixes, and PUB can drop early messages before subscription handshakes finish. Set RCVTIMEO, SNDTIMEO, and high-water marks instead of allowing every call and queue to wait forever.
Shutdown needs an explicit policy. Unsent frames can hold close or context termination according to LINGER, so use a finite value and close sockets before terminating their context. send_pyobj and recv_pyobj use pickle; the pyzmq docs warn that an untrusted sender can execute code during deserialization. Prefer bytes, JSON, or a validated codec. CURVE encrypts and authenticates transport, but client authorization still requires an authenticator and a managed allowlist of public keys.
Patterns
Exchange one request and one reply request-reply
# server.py
import zmq
ctx = zmq.Context()
rep = ctx.socket(zmq.REP)
rep.bind('tcp://127.0.0.1:5555')
while True:
request = rep.recv_json()
rep.send_json({'id': request['id'], 'ok': True})
# client.py
req = ctx.socket(zmq.REQ)
req.connect('tcp://127.0.0.1:5555')
req.send_json({'id': 42})
reply = req.recv_json()REQ and REP enforce alternating send and receive calls. A timeout does not reset that state machine; recreate or relax the pattern when retries are required.
Publish multipart messages by topic publish-topics
# publisher
pub = ctx.socket(zmq.PUB)
pub.bind('tcp://*:5556')
pub.send_multipart([b'orders.created', b'{"id":42}'])
# subscriber
sub = ctx.socket(zmq.SUB)
sub.connect('tcp://127.0.0.1:5556')
sub.subscribe(b'orders.')
topic, payload = sub.recv_multipart()SUB filters by byte prefix and receives nothing without a subscription. Messages sent before the subscription handshake may be lost.
Fan jobs out with PUSH and PULL distribute-work
# producer
push = ctx.socket(zmq.PUSH)
push.bind('tcp://*:5557')
for job_id in range(100):
push.send_json({'job_id': job_id})
# worker
pull = ctx.socket(zmq.PULL)
pull.connect('tcp://127.0.0.1:5557')
while True:
job = pull.recv_json()
process(job)PUSH balances messages among connected PULL sockets. It supplies no acknowledgement or automatic redelivery when a worker dies after receiving a job.
Address DEALER peers through a ROUTER route-by-identity
# broker
router = ctx.socket(zmq.ROUTER)
router.bind('tcp://*:5558')
peer_id, payload = router.recv_multipart()
router.send_multipart([peer_id, b'accepted:' + payload])
# worker
dealer = ctx.socket(zmq.DEALER)
dealer.setsockopt(zmq.IDENTITY, b'worker-7')
dealer.connect('tcp://127.0.0.1:5558')
dealer.send(b'job-42')
reply = dealer.recv()ROUTER adds the sender identity as the first frame and requires that identity on the reply. Mixed REQ and DEALER peers may use different envelope frames.
Wait on several sockets with a deadline poll-sockets
poller = zmq.Poller()
poller.register(sub, zmq.POLLIN)
poller.register(pull, zmq.POLLIN)
events = dict(poller.poll(timeout=1000))
if sub in events:
handle_event(sub.recv_multipart())
if pull in events:
handle_job(pull.recv_json())
if not events:
send_heartbeat()Poller timeouts are measured in milliseconds. A quiet interval returns an empty event list and does not indicate a disconnected peer.
Receive ZeroMQ messages with asyncio await-messages
import asyncio
import zmq
import zmq.asyncio
async def consume():
ctx = zmq.asyncio.Context()
sock = ctx.socket(zmq.PULL)
sock.bind('tcp://127.0.0.1:5559')
try:
while True:
message = await sock.recv_multipart()
await handle(message)
finally:
sock.close(linger=0)
ctx.term()
asyncio.run(consume())Use zmq.asyncio sockets for Future-returning operations. Keep one receive loop per socket instead of racing several coroutines on recv().
Stop send and receive from waiting forever bound-wait-time
sock.setsockopt(zmq.RCVTIMEO, 2000)
sock.setsockopt(zmq.SNDTIMEO, 2000)
try:
sock.send_json(payload)
reply = sock.recv_json()
except zmq.Again:
record_timeout()Both timeout options use milliseconds and raise zmq.Again. A timed-out REQ socket remains in its request state and cannot simply send again.
Apply send and receive high-water marks limit-queued-messages
sock.setsockopt(zmq.SNDHWM, 5000)
sock.setsockopt(zmq.RCVHWM, 5000)
try:
sock.send_multipart(frames, flags=zmq.NOBLOCK)
except zmq.Again:
metrics.increment('zmq.queue_full')Set high-water marks before bind or connect. Some socket types block or raise at the limit, while PUB can drop messages for slow subscribers.
Send structured data without pickle serialize-safe-payload
import json
payload = json.dumps({'id': 42, 'ok': True}).encode('utf-8')
sock.send(payload)
message = json.loads(sock.recv().decode('utf-8'))recv_pyobj calls pickle.loads and can execute attacker-controlled code. Authenticate the peer and prefer a constrained codec for untrusted boundaries.
Create CURVE credentials from the command line generate-curve-keys
python -m zmq.curve_keygen --jsonVersion 27.2 adds this entry point. Store secret keys with restricted permissions; the public server key must be distributed to clients through a trusted channel.
Configure a CURVE client and server secure-curve-transport
server_public, server_secret = zmq.curve_keypair()
client_public, client_secret = zmq.curve_keypair()
server = ctx.socket(zmq.REP)
server.curve_secretkey = server_secret
server.curve_publickey = server_public
server.curve_server = True
server.bind('tcp://*:5560')
client = ctx.socket(zmq.REQ)
client.curve_publickey = client_public
client.curve_secretkey = client_secret
client.curve_serverkey = server_public
client.connect('tcp://127.0.0.1:5560')CURVE options must be set before the endpoint connects. Use a zmq.auth authenticator when the server must allow only named client public keys.
Shut sockets down with finite linger close-with-deadline
ctx = zmq.Context()
sock = ctx.socket(zmq.PUSH)
sock.setsockopt(zmq.LINGER, 1000)
sock.connect('tcp://127.0.0.1:5557')
try:
sock.send_json({'job_id': 42})
finally:
sock.close()
ctx.term()A 1000 ms linger gives pending sends a bounded drain period. linger=0 drops queued frames immediately and is appropriate only when that loss is accepted.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pynng | PyPI | Choose it for NNG messaging patterns when that C library and its dialer or listener model fit the deployment |
| aiozmq | PyPI | Choose it only when maintaining an older asyncio integration that already depends on its stream and RPC abstractions |
| pika | PyPI | Choose it with RabbitMQ when durable queues, acknowledgements, routing exchanges, and broker operations are requirements |
| nats-py | PyPI | Choose it when a NATS server can own subject routing, reconnect behavior, and optional JetStream durability |
More web backend guides
urllib3 · requests · ws · anyio · httpx · undici · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

