932k msg/s inproc | 328k msg/s ipc | 329k msg/s tcp
11.5 µs inproc latency | 54 µs ipc | 69 µs tcp
Ruby 4.0 + YJIT on a Linux VM — see
bench/for full results
gem install omq and you're done. No libzmq, no compiler, no system packages — just Ruby talking to every other ZeroMQ peer out there.
ØMQ gives your Ruby processes a way to talk to each other — and to anything else speaking ZeroMQ — without a broker in the middle. Same API whether they live in the same process, on the same machine, or across the network. Reconnects, queuing, and back-pressure are handled for you; you write the interesting part.
New to ZeroMQ? Start with GETTING_STARTED.md — a ~30 min walkthrough of every major pattern with working code.
- Zero dependencies on C — no extensions, no FFI, no libzmq.
gem installjust works everywhere - Fast — YJIT-optimized hot paths, batched sends, GC-tuned allocations, buffered I/O via io-stream, direct-pipe inproc bypass
omqCLI — a powerful swiss army knife for ØMQ.gem install omq-cli- Every socket pattern — req/rep, pub/sub, push/pull, dealer/router, xpub/xsub, pair, and all draft types
- Every transport — tcp, ipc (Unix domain sockets), inproc (in-process queues)
- Async-native — built on fibers, non-blocking from the ground up
- Works outside Async too — a shared IO thread handles sockets for callers that aren't inside a reactor, so simple scripts just work
- Wire-compatible — interoperates with libzmq, pyzmq, CZMQ, zmq.rs over tcp and ipc
- Bind/connect order doesn't matter — connect before bind, bind before connect, peers come and go. ZeroMQ reconnects automatically and queued messages drain when peers arrive
For architecture internals, see DESIGN.md.
No system libraries needed — just Ruby:
gem install omq
# or in Gemfile
gem 'omq'require 'omq'
require 'async'
Async do |task|
rep = OMQ::REP.bind('inproc://example')
req = OMQ::REQ.connect('inproc://example')
task.async do
msg = rep.receive
rep << msg.map(&:upcase)
end
req << 'hello'
p req.receive # => ["HELLO"]
ensure
req&.close
rep&.close
endAsync do |task|
pub = OMQ::PUB.bind('inproc://pubsub')
sub = OMQ::SUB.connect('inproc://pubsub')
sub.subscribe('') # subscribe to all
task.async { pub << 'news flash' }
p sub.receive # => ["news flash"]
ensure
pub&.close
sub&.close
endAsync do
push = OMQ::PUSH.connect('inproc://pipeline')
pull = OMQ::PULL.bind('inproc://pipeline')
push << 'work item'
p pull.receive # => ["work item"]
ensure
push&.close
pull&.close
endOMQ spawns a shared omq-io thread when used outside an Async reactor — no boilerplate needed:
require 'omq'
push = OMQ::PUSH.bind('tcp://127.0.0.1:5557')
pull = OMQ::PULL.connect('tcp://127.0.0.1:5557')
push << 'hello'
p pull.receive # => ["hello"]
push.close
pull.closeThe IO thread runs all pumps, reconnection, and heartbeating in the background. When you're inside an Async block, OMQ uses the existing reactor instead.
All sockets expose an Async::Queue-inspired interface:
| Async::Queue | OMQ Socket | Notes |
|---|---|---|
enqueue(item) / push(item) |
enqueue(msg) / push(msg) |
Also: send(msg), << msg |
dequeue(timeout:) / pop(timeout:) |
dequeue(timeout:) / pop(timeout:) |
Defaults to socket's read_timeout |
wait |
wait |
Blocks indefinitely (ignores read_timeout) |
each |
each |
Yields messages; returns on close or timeout |
pull = OMQ::PULL.bind('inproc://work')
# iterate messages like a queue
pull.each do |msg|
puts msg.first
endAll sockets are thread-safe. Default HWM is 1000 messages per socket. max_message_size defaults to nil (unlimited) — set socket.max_message_size = N to cap inbound frames at N bytes; oversized frames cause the connection to be dropped before the body is read from the wire. Classes live under OMQ:: (alias: ØMQ).
| Pattern | Send | Receive | When HWM full |
|---|---|---|---|
| REQ / REP | Work-stealing / route-back | Fair-queue | Block |
| PUB / SUB | Fan-out to subscribers | Subscription filter | Drop |
| PUSH / PULL | Work-stealing to workers | Fair-queue | Block |
| DEALER / ROUTER | Work-stealing / identity-route | Fair-queue | Block |
| XPUB / XSUB | Fan-out (subscription events) | Fair-queue | Drop |
| PAIR | Exclusive 1-to-1 | Exclusive 1-to-1 | Block |
Work-stealing vs. round-robin. libzmq uses strict per-pipe round-robin for outbound load balancing — message N goes to peer N mod K regardless of whether that peer is busy. OMQ uses work-stealing: one shared send queue per socket and N pump fibers that race to drain it. Whichever pump is ready next picks up the next batch, so a slow peer can't stall the pipeline. The trade-off: distribution is not strict round-robin under bursts. If a producer enqueues a large burst before any pump fiber gets scheduled, the first pump to wake will dequeue up to one whole batch (256 messages or 512 KB, whichever hits first) in a single non-blocking drain — so a tight
n.times { sock << msg }loop on a smallnmay dump everything on one peer. Slow or steady producers don't see this: each pump dequeues one message, writes, re-parks, and the FIFO wait queue gives every pump a fair turn. Burst distribution also evens out once the burst exceeds one pump's batch cap. See DESIGN.md for the full reasoning.
Each draft pattern lives in its own gem — install only the ones you use.
| Pattern | Send | Receive | When HWM full | Gem |
|---|---|---|---|---|
| CLIENT / SERVER | Work-stealing / routing-ID | Fair-queue | Block | omq-rfc-clientserver |
| RADIO / DISH | Group fan-out | Group filter | Drop | omq-rfc-radiodish |
| SCATTER / GATHER | Work-stealing | Fair-queue | Block | omq-rfc-scattergather |
| PEER | Routing-ID | Fair-queue | Block | omq-rfc-p2p |
| CHANNEL | Exclusive 1-to-1 | Exclusive 1-to-1 | Block | omq-rfc-channel |
Install omq-cli for a command-line tool that sends, receives, pipes, and transforms ZeroMQ messages from the terminal:
gem install omq-cli
omq rep -b tcp://:5555 --echo
echo "hello" | omq req -c tcp://localhost:5555See the omq-cli README for full documentation.
- omq-ffi — libzmq FFI backend. Same OMQ socket API, but backed by libzmq instead of the pure Ruby ZMTP stack. Useful for interop testing and when you need libzmq-specific features. Requires libzmq installed.
- omq-ractor — bridge OMQ sockets into Ruby Ractors for true parallel processing across cores. I/O stays on the main Ractor, worker Ractors do pure computation.
Optional plug-ins that extend the ZMTP wire protocol. Each is a separate gem; load the ones you need.
- omq-rfc-zstd — transparent Zstandard compression on the wire, negotiated per peer via READY properties.
bundle install
bundle exec rakeSet OMQ_DEV=1 to tell Bundler to load sibling projects from source
(protocol-zmtp, nuckle, omq-rfc-*, etc.) instead of released gems.
This is required for running benchmarks and for testing changes across
the stack.
# clone OMQ and its sibling repos into the same parent directory
git clone https://github.com/paddor/omq.git
git clone https://github.com/paddor/protocol-zmtp.git
git clone https://github.com/paddor/omq-rfc-zstd.git
git clone https://github.com/paddor/omq-rfc-clientserver.git
git clone https://github.com/paddor/omq-rfc-radiodish.git
git clone https://github.com/paddor/omq-rfc-scattergather.git
git clone https://github.com/paddor/omq-rfc-channel.git
git clone https://github.com/paddor/omq-rfc-p2p.git
git clone https://github.com/paddor/omq-rfc-qos.git
git clone https://github.com/paddor/omq-ffi.git
git clone https://github.com/paddor/omq-ractor.git
git clone https://github.com/paddor/nuckle.git
cd omq
OMQ_DEV=1 bundle install
OMQ_DEV=1 bundle exec rake