A minimal, scriptable benchmarking tool for pub/sub and request/reply with CSV metrics. Built in Rust with Tokio. Ships a Docker Compose stack with MQTT (Mosquitto/EMQX/HiveMQ), Redis, NATS, RabbitMQ, ActiveMQ Artemis, and an optional Zenoh 3-router star.
The harness uses a pluggable transport abstraction with adapters for MQTT, Redis, NATS, Zenoh (1.5.1), and RabbitMQ (AMQP via lapin). Select an engine via --engine and pass connection options via --connect KEY=VALUE.
- Docker Compose stack: MQTT (Mosquitto/EMQX/HiveMQ), Redis, NATS, RabbitMQ, ActiveMQ Artemis, plus an optional Zenoh 3-router star (router2 → router1 ← router3)
- Roles: pub, sub, req, qry; plus multi-topic mt-pub/mt-sub
- Open-loop publisher with rate control
- Requester with QPS pacing, concurrency, and timeouts
- Subscriber with latency measurement (ns timestamps embedded in payload)
- CSV snapshots to stdout or file (
--csv)
Performance-focused implementation details
- Handler-based subscribe and query registration (lower overhead than streams)
- Reusable declared publisher for hot-path sends
- Payload header is fixed 24 bytes; subscribers decode for E2E latency
- Batching and CSV flushing for live tailing
Transports (current)
- mqtt, redis, nats, zenoh, rabbitmq (AMQP)
- rabbitmq: native AMQP adapter (alias:
amqp). Connect via--connect url=amqp://user:pass@host:port/%2f. - rabbitmq-mqtt: use RabbitMQ’s MQTT plugin through the MQTT adapter (host: 127.0.0.1, port: 1886 by default).
- artemis: alias to MQTT adapter to hit Artemis’s MQTT listener (127.0.0.1:1887). You can also use
mqtt-artemistransport. - Broker-specific MQTT aliases are available:
mqtt-mosquitto,mqtt-emqx,mqtt-hivemq,mqtt-rabbitmq,mqtt-artemis. - Wildcards: use slash style in CLI (e.g.,
bench/**). Adapters map to engine-native patterns (e.g., NATSa.>; AMQP usesamq.topicwith dot routing internally). - Topics: use slash form in CLI (e.g.,
bench/topic); adapters map as needed (e.g., NATS and AMQP use dots under the hood).
- rabbitmq: native AMQP adapter (alias:
- Bring up services (MQTT/Redis/NATS; Zenoh routers optional)
# From repo root
docker compose up -d
# Verify ports (bound to 127.0.0.1)
# Redis: 6379 | NATS: 4222 | MQTT: 1883/1884/1885 | RabbitMQ: AMQP 5672, MQTT 1886 | Artemis: AMQP 5673, MQTT 1887 | Zenoh: 7447/7448/7449 (optional)- Build the harness
cargo build --release- Run quick pub/sub sanity tests
MQTT (defaults to Mosquitto on 1883)
./target/release/mq-bench sub --engine mqtt --connect host=127.0.0.1 --connect port=1883 --expr bench/topic
./target/release/mq-bench pub --engine mqtt --connect host=127.0.0.1 --connect port=1883 --topic-prefix bench/topic --payload 200 --rate 5 --duration 10Redis
./target/release/mq-bench sub --engine redis --connect url=redis://127.0.0.1:6379 --expr bench/topic
./target/release/mq-bench pub --engine redis --connect url=redis://127.0.0.1:6379 --topic-prefix bench/topic --payload 200 --rate 5 --duration 10NATS (default port 4222)
./target/release/mq-bench sub --engine nats --connect host=127.0.0.1 --connect port=4222 --expr bench/topic
./target/release/mq-bench pub --engine nats --connect host=127.0.0.1 --connect port=4222 --topic-prefix bench/topic --payload 200 --rate 5 --duration 10Zenoh (optional cross-router: sub on router2/7448, pub to router3/7449)
./target/release/mq-bench sub --endpoint tcp/127.0.0.1:7448 --expr bench/topic
./target/release/mq-bench pub --endpoint tcp/127.0.0.1:7449 --payload 200 --rate 5 --duration 10You should see subscriber logs like:
DEBUG: Received message seq=12, latency=1.10ms
Subscriber stats - Received: 20, Errors: 0, Rate: 4.00 msg/s, P99 Latency: 3.01ms
Top-level:
--run-id STRINGtag outputs/CSV (optional)--out-dir PATHbase artifacts directory (default./artifacts)--log-level trace|debug|info|warn|error(defaultinfo)--snapshot-interval SECSstats snapshot cadence (default1)
All roles accept a transport engine and connection options:
-
--engine zenoh|mqtt|redis|nats|rabbitmq(alias:amqp) -
--connect KEY=VALUE(repeatable) -
Zenoh back-compat:
--endpoint tcp/host:portmaps to--connect endpoint=... -
Publisher (pub)
- Topic/key selection
--topic-prefixbase key (defaults tobench/topic)- Multi-topic:
--topics Nand--publishers Mto create M logical publishers over N topics
--payloadsize in bytes--ratemessages per second (omitted or <= 0 means unlimited)--durationseconds
--csv path/to/pub.csvto write CSV snapshots to a file (stdout if omitted)
- Topic/key selection
-
Subscriber (sub)
--exprkey expression, e.g.bench/topicorbench/**--csv path/to/sub.csvto write CSV snapshots to a file (stdout if omitted)
-
Requester (req)
--key-exprquery key expression--qpsqueries per second (omitted or <= 0 means unlimited)--concurrencymax in-flight--timeoutper-query timeout (ms)--durationseconds--csv path/to/req.csv
-
Queryable (qry)
--serve-prefixrepeatable; prefixes to serve--reply-sizereply body size in bytes--proc-delayprocessing delay per query (ms)--csv path/to/qry.csv
-
Multi-topic publisher (mt-pub)
--topic-prefixbase key (e.g.,bench/topic)- Dimensions:
--tenants T --regions R --services S --shards K --publishers Mlogical publishers (<= TRS*K; -1 uses total keys)--mapping mdim|hashkey mapping across publishers--payload,--rate,--duration,--csv,--share-transport
-
Multi-topic subscriber (mt-sub)
--topic-prefixbase key (e.g.,bench/mtopic)- Dimensions:
--tenants T --regions R --services S --shards K --subscribers Nnumber of per-key subscriptions (<= total keys; -1 uses total keys)--mapping mdim|hash,--duration,--csv,--share-transport
Tip: If you pass --csv ./artifacts/run1/pub.csv or sub.csv, parent directories will be created automatically.
Engine connect hints:
- MQTT:
--connect host=127.0.0.1 --connect port=1883- Optional auth: add
--connect username=USER --connect password=PASS
- Optional auth: add
- Redis:
--connect url=redis://127.0.0.1:6379 - NATS:
--connect host=127.0.0.1 --connect port=4222 - Zenoh:
--endpoint tcp/127.0.0.1:7448(or--connect endpoint=tcp/127.0.0.1:7448) - RabbitMQ (AMQP):
--connect url=amqp://guest:[email protected]:5672/%2f - RabbitMQ (MQTT plugin): use MQTT engine at host 127.0.0.1 port 1886
- Artemis (MQTT): use MQTT engine at host 127.0.0.1 port 1887
With services up and the binary built:
- Start a queryable on Zenoh router2 (optional, 7448):
./target/release/mq-bench qry --endpoint tcp/127.0.0.1:7448 --serve-prefix bench/topic --reply-size 256 --proc-delay 0- Run a requester on Zenoh router3 (7449):
./target/release/mq-bench req --endpoint tcp/127.0.0.1:7449 --key-expr bench/topic --qps 1000 --concurrency 32 --timeout 2000 --duration 5NATS
# Start a responder (subscribe and reply)
./target/release/mq-bench qry --engine nats --connect host=127.0.0.1 --connect port=4222 \
--serve-prefix bench/topic --reply-size 256 --proc-delay 0
# Run a requester
./target/release/mq-bench req --engine nats --connect host=127.0.0.1 --connect port=4222 \
--key-expr bench/topic --qps 1000 --concurrency 32 --timeout 2000 --duration 5Redis (simple list-based req/rep baseline)
./target/release/mq-bench qry --engine redis --connect url=redis://127.0.0.1:6379 \
--serve-prefix bench/topic --reply-size 256 --proc-delay 0
./target/release/mq-bench req --engine redis --connect url=redis://127.0.0.1:6379 \
--key-expr bench/topic --qps 1000 --concurrency 32 --timeout 2000 --duration 5Notes:
- Start the queryable before the requester.
- Add
--csv path/to/req.csvorqry.csvto save snapshots. - RabbitMQ AMQP: request/reply is not implemented yet in the adapter; pub/sub is supported.
Publish over many keys and subscribe to all of them in one process:
# Subscriber over many keys
./target/release/mq-bench mt-sub --engine mqtt --connect host=127.0.0.1 --connect port=1883 \
--topic-prefix bench/mtopic --tenants 4 --regions 2 --services 3 --shards 8 \
--subscribers -1 --duration 10
# Matching publisher
./target/release/mq-bench mt-pub --engine mqtt --connect host=127.0.0.1 --connect port=1883 \
--topic-prefix bench/mtopic --tenants 4 --regions 2 --services 3 --shards 8 \
--publishers -1 --payload 256 --rate 1000 --duration 10Docker Compose defines optional services (all bound to 127.0.0.1):
- MQTT brokers: Mosquitto 1883, EMQX 1884, HiveMQ 1885, RabbitMQ MQTT 1886, Artemis MQTT 1887
- RabbitMQ: AMQP 5672
- Artemis: AMQP 5673
- Redis: 6379
- NATS: 4222
- Zenoh star (optional): 7447; 7448→7447; 7449→7447
Configs in config/ have discovery disabled to ensure deterministic static peering.
- Generic, multi-transport:
scripts/run_baseline.sh(ENGINE=zenoh|mqtt|redis|nats|rabbitmq)scripts/run_fanout.sh(ENGINE=zenoh|mqtt|redis|nats|rabbitmq)
- Convenience wrappers:
- Baseline:
run_mqtt_baseline.sh,run_redis_baseline.sh,run_nats_baseline.sh,run_rabbitmq_baseline.sh,run_artemis_baseline.sh - Fanout:
run_mqtt_fanout.sh,run_redis_fanout.sh,run_nats_fanout.sh,run_rabbitmq_fanout.sh,run_artemis_fanout.sh
- Baseline:
Examples for RabbitMQ and Artemis:
RabbitMQ (AMQP)
./target/release/mq-bench sub --engine rabbitmq --connect url=amqp://guest:[email protected]:5672/%2f --expr bench/topic
./target/release/mq-bench pub --engine rabbitmq --connect url=amqp://guest:[email protected]:5672/%2f --topic-prefix bench/topic --payload 200 --rate 5 --duration 10RabbitMQ (MQTT plugin)
./target/release/mq-bench sub --engine mqtt --connect host=127.0.0.1 --connect port=1886 --expr bench/topic
./target/release/mq-bench pub --engine mqtt --connect host=127.0.0.1 --connect port=1886 --topic-prefix bench/topic --payload 200 --rate 5 --duration 10Artemis (MQTT)
./target/release/mq-bench sub --engine mqtt --connect host=127.0.0.1 --connect port=1887 --expr bench/topic
./target/release/mq-bench pub --engine mqtt --connect host=127.0.0.1 --connect port=1887 --topic-prefix bench/topic --payload 200 --rate 5 --duration 10Minimal steps to introduce a new engine (e.g., "foo"):
- Adapter: create
src/transport/foo.rsthat implementsTransport(must supportsubscribeandcreate_publisher;request/register_queryablecan return NotImplemented initially). Parse--connect KEY=VALUEfromConnectOptions.paramsand pass payload bytes through unchanged. - Wire-up in code:
src/transport/mod.rs: addEngine::Fooand callfoo::connect(...)under a#[cfg(feature = "transport-foo")]gate.src/transport/config.rs: map engine strings (e.g., "foo") inparse_enginetoEngine::Foo.Cargo.toml: add featuretransport-fooenabling the adapter’s deps.
- Scripts: in
scripts/lib.sh, teachmake_connect_argshow to turnENGINE=foointo--engine foo --connect ...pairs; optionally add cases inscripts/orchestrate_benchmarks.shfor baseline/fanout labels. - Docker (optional): add a service to
docker-compose.ymland bind ports to 127.0.0.1. - Docs: list the engine under “Transports (current)” with example connect hints.
Tip: keep topic names in slash style at the CLI; if the backend uses a different pattern (e.g., dots), translate internally inside the adapter.