ØMQ — ZeroMQ for Ruby, no C required
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.
Highlights
- 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.
Install
No system libraries needed — just Ruby:
gem install omq
# or in Gemfile
gem 'omq'
Quick Start
Request / Reply
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
end
Pub / Sub
Async 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
end
Push / Pull (Pipeline)
Async 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
end
Without Async (IO thread)
OMQ 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.close
The IO thread runs all pumps, reconnection, and heartbeating in the background. When you're inside an Async block, OMQ uses the existing reactor instead.
Queue Interface
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
end
Socket Types
All 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).
Standard (multipart messages)
| 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.
Draft (single-frame only)
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 |
CLI
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:5555
See the omq-cli README for full documentation.
Companion Gems
- 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.
Protocol extensions (RFCs)
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.
Development
bundle install
bundle exec rake
Full development setup
Set 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