QueenMQ speaks JSON over HTTP on port 6632, so every client is a thin layer over the same routes, and curl is a complete client. An SDK adds the parts that are tedious by hand: consume loops that ack for you, lease renewal, push buffering, retries with failover across the nodes of a cluster, and builders for transactions, KV, timers and locks.
Install and connect
npm install queen-mqimport { Queen } from 'queen-mq'
const queen = new Queen({ url: 'http://localhost:6632' })
// ...
await queen.close()Node.js 24 or later. With urls: [...] instead of url, the client sends each consumer’s pops to
the node that a consistent hash of its queue, partition and group picks (the default affinity
strategy; round-robin and session are the others), and fails over to another node on a network
error or a 5xx.
README
pip install queen-mqfrom queen import Queen
async def main():
queen = Queen(url="http://localhost:6632")
# ... every call is awaited
await queen.close()Python 3.9 or later. The client is asyncio from end to end, and url is a keyword argument.
README
go get github.com/smartpricing/queen/clients/client-go/v2import queen "github.com/smartpricing/queen/clients/client-go/v2"
client, err := queen.New("http://localhost:6632")
if err != nil {
return err
}
defer client.Close(context.Background())Go 1.24 or later. queen.New also takes a slice of URLs or a ClientConfig. The repository
lives at github.com/queen-mq/queen, but the module path still carries the organization it
started in, so import it exactly as written above.
README
cargo add queen-mq serde_json
cargo add tokio --features rt-multi-thread,macrosuse queen_mq::{Config, Queen};
let queen = Queen::connect(Config::new("http://localhost:6632"))?;Rust 1.86 or later, on tokio. connect checks the configuration and opens no socket; the first
request does.
README
The client is one header, clients/client-cpp/queen_client.hpp, used from a checkout of the
repository: it includes the thread pool in clients/server/include/ by a relative path, and
clients/server/vendor/ carries the nlohmann/json it needs. You install cpp-httplib 0.27.0 or later and OpenSSL
(brew install cpp-httplib openssl on macOS).
#include "queen_client.hpp"
queen::QueenClient client("http://localhost:6632");
// ...
client.close();g++ -std=c++17 -O2 -pthread \
-I clients/client-cpp -I clients/server/vendor \
-I /opt/homebrew/include -I "$(brew --prefix openssl)/include" \
app.cpp -o app \
-L "$(brew --prefix openssl)/lib" -lssl -lcrypto -lpthreadC++17 or later. Link OpenSSL even over plain HTTP: the header turns on cpp-httplib’s TLS support
itself. Fluent consumers can enable dashboard observations with
.supervision(queen::SupervisionConfig{"billing-production"}) before .consume(...);
they are off by default. The README describes worker counts, handler progress and publication
timings. These observations do not restart or scale application processes.
README
composer require queen-mq/php-clientuse Queen\Queen;
$queen = new Queen('http://localhost:6632');PHP 8.3 or later. The same package carries the Laravel integration: package discovery registers a
queen queue connection, and your jobs stay ordinary Laravel jobs.
QUEUE_CONNECTION=queen
QUEEN_URL=http://127.0.0.1:6632See Laravel, and migrate from Horizon if you run Horizon today. README
The six clients are at 2.6.0 and talk to any 2.x broker. Locks need a 2.1 broker, and the binary forms a 2.3 one.
How each SDK names things
These pages name options and methods as the JavaScript client spells them. The others follow the habits of their language:
| JavaScript | Python | Go | Rust | PHP | C++ |
|---|---|---|---|---|---|
leaseTime |
lease_time |
LeaseTime |
lease_time |
leaseTime |
lease_time |
subscriptionMode('all') |
subscription_mode("all") |
SubscriptionMode(queen.SubscriptionModeAll) |
subscription_mode(SubscriptionMode::All) |
subscriptionMode('all') |
subscription_mode("all") |
idleMillis(1500) |
idle_millis(1500) |
IdleMillis(1500) |
idle_millis(1500) |
idleMillis(1500) |
idle_millis(1500) |
ttl: '30d' |
ttl=timedelta(days=30) |
a time.Duration |
a Duration |
ttlSeconds |
KvTtl |
await on every call |
await (asyncio) |
ctx on every call |
.await (tokio) |
blocking | blocking |
Where the SDKs differ
The core is the same everywhere: push, pop, consume, ack, transactions, KV, timers and locks. These are the differences the programs behind these pages ran into, on 2.6.0:
| Topic | JavaScript | The others |
|---|---|---|
| Streams | Yes | Python, Go and Rust too. PHP and C++ have none. |
Consumer callbacks (onSuccess, onError) |
Yes | Python and PHP too. Go, Rust and C++ ack in the handler. |
| A refused message in a push | Throws | Returned as that message’s status. |
| A batch ack with mixed statuses | One call | Python and PHP too. Go, Rust and C++: one call per status, or a transaction of acks. |
| Stopping a consume loop | At once; unstarted messages go back with retry |
After the current message; the rest wait for their lease. |
| zstd for the binary forms | Built in | Python 3.14 or queen-mq[zstd], PHP ext-zstd, C++ -lzstd; Go and Rust built in. |
Where an SDK lacks a method, the route is still there over HTTP: the C++ code on these pages calls a few routes directly, and says why in a comment.
Parity here is a claim about the surface, not about shared code: each client is written natively
in its own idiom. These nineteen rows are that surface, read out of clients/ on this tree. Nine
of them are full parity across all six clients; the other ten are where a client stops.
| Capability | JavaScript | Python | Go | Rust | PHP | C++ |
|---|---|---|---|---|---|---|
| Push, pop, ack, multi-partition claim | yes | yes | yes | yes | yes | yes |
| Client-side push buffering | yes | yes | yes | yes | yes | yes |
Transactions through POST /api/v1/transaction |
yes | yes | yes | yes | yes | yes |
| Dead-letter reader | yes | yes | yes | yes | yes | yes |
Reads by offset: fetch and changedSince |
yes | yes | yes | yes | yes | yes |
| HTTP 429 policy, separate from the retry counter | yes | yes | yes | yes | yes | yes |
kv and timers riders on a transaction |
yes | yes | yes | yes | yes | yes |
| Key/value: the eight operations | yes | yes | yes | yes | yes | six of eight |
Timers: schedule, cancel, peek, list |
yes | yes | yes | yes | yes | two of four |
once, the idempotency marker in one call |
yes | yes | no | no | no | no |
Locks and semaphores, with guard on a transaction |
yes | yes | yes | yes | yes | yes |
| Lock renewal in the background | yes | yes | yes | yes | no | no |
Admin facade |
yes | yes | yes | yes | yes | no |
affinity load-balancing strategy |
yes | yes | yes | yes | yes | no |
Streams DSL over /streams/v1/* |
yes | yes | yes | yes | no | no |
| Lease renewal inside the consume loop | yes | yes | yes | yes | yes | yes |
Per-message trace helper |
yes | yes | yes | no | yes | no |
Host override independent of the address dialled |
yes | no | no | yes | no | no |
pop reports a failure instead of an empty result |
no | no | yes | yes | yes | no |
The ten divergent rows, with their conditions:
- Key/value. The eight operations are
get,getMany,getPrefix,put,putIfAbsent,delete,incrandcheck. C++ wraps six of them:getManyandgetPrefixhave no method, and both remain reachable throughPOST /api/v1/kvon the sharedHttpClient.getPrefixinside a transaction is refused by the broker in every client, which is its rule and not a client’s gap. - Timers. C++ has
scheduleandcancelonly;peekandlistare reads and stay onGET /api/v1/timers/{queue}[/{timerKey}]. There is no reschedule operation anywhere, becausescheduleis the upsert andstatusreports which happened; Python, Go, Rust and PHP still spell areschedulealias over it, JavaScript and C++ do not. once. JavaScript (kv.once,transaction().once) and Python (kv.once,transaction().once) foldputIfAbsentplusrequiredinto the question people actually ask, “did I win?”. The other four write theputIfAbsentthemselves, which is the same wire and one line longer.- Lock renewal. A lock is a lease its holder renews. The JavaScript, Python, Go and Rust
handles renew by themselves, every third of the lifetime, and signal a lock that was lost. PHP
and C++ have no background task to renew from: their handle has
keepAlive()(keep_alive()in C++), a checkpoint to call inside the work loop, which renews when a third of the lifetime has passed and returns false once the lock is gone. A PHP or C++ holder that never calls it keeps the lock for one lifetime only. kvon a streams operator context. No client has it, so it is not a row. Inside a stream the state primitive isstate_ops, which commits with the sink and the ack in the cycle’s own transaction; a key/value write from an operator would not, and that atomicity is the thing the stream already gives you for free.Adminfacade. C++ has noAdminclass. Every management and observability route is still reachable, through the sharedHttpClientreturned byget_http_client().affinity. The C++LoadBalancerimplements round-robin and"session"only, andload_balancing_strategy = "affinity"falls through to round-robin silently. The strategy matters because it keeps one consumer’s pops on one backend, which is what keeps two clients from contending on the same partition claim.- Streams. The
Streambuilder, the four window kinds, gates and the/streams/v1/*runtime are in the JavaScript, Python, Go and Rust clients. PHP and C++ have none of it. - Per-message
trace. Where it exists it is attached by the consume loop, so a message returned by a manualpop()never carries it. Rust records a trace throughAdmin::record_traceinstead of hanging a method off the message. C++ cannot hang one off a JSON object at all, and its trace hook is an explicit no-op. Hostoverride. JavaScript (hostHeader) and Rust (host_header) advertise a request authority, and a TLS SNI, independent of the address the socket dials. That is what addresses a named tenant cluster behind a shared proxy endpoint. The other four clients send the dialled address.popfailures. Go, Rust and PHP surface a 4xx, an exhausted 429 budget, a terminal 403 or a network fault to the caller, so an empty result means an empty queue. JavaScript, Python and C++ log the failure and return an empty result, which does not distinguish an empty queue from revoked credentials.
curl and queenctl
Every SDK call is one of the broker’s routes, so anything that can send an HTTP request is a client:
curl -s -X POST http://localhost:6632/api/v1/push -H 'content-type: application/json' \
-d '{"items":[{"queue":"orders","partition":"customer-123","payload":{"orderId":8891}}]}'The HTTP reference lists every route. queenctl, the operator CLI (status, tail, push, replay, lag, dead letters), ships inside the Docker image (docker exec queen queenctl status) and as release binaries. See queenctl.
Binary forms, by SDK
The three options of binary push, compression and framed pop are spelled:
| Client | Options |
|---|---|
| JavaScript | binaryPush, pushCompression, framedPop in the Queen config |
| Python | binary_push, push_compression, framed_pop, keyword arguments of Queen |
| Go | BinaryPush, PushCompression, FramedPop in ClientConfig |
| Rust | .binary_push(true), .push_compression(true), .framed_pop(true) on Config |
| C++ | binary_push, push_compression, framed_pop in ClientConfig |
| PHP | binaryPush, pushCompression, framedPop in the Queen config; under Laravel binary_push, push_compression, framed_pop in config/queen.php |
Reads by offset, by SDK
fetch reads a partition by position, and changedSince lists the partitions written since a time. Neither takes a lease or moves a cursor.
| Client | Read | Discover |
|---|---|---|
| JavaScript | queue(q).fetch(partitions, { offsets, maxBytes }) |
queue(q).changedSince(since) |
| Python | queue(q).fetch(partitions, offsets=..., max_bytes=...) |
queue(q).changed_since(since) |
| Go | Queue(q).Fetch(ctx, partitions, FetchOptions{...}) |
Queue(q).ChangedSince(ctx, since) |
| Rust | queue(q).fetch(partitions), fetch_with(partitions, FetchOptions) |
queue(q).changed_since(since) |
| C++ | queue(q).fetch(partitions, FetchOptions) |
queue(q).changed_since(since) |
| PHP | queue($q)->fetch($partitions, $offsets, $maxBytes) |
queue($q)->changedSince($since) |
changedSince answers a safeTime: give it as the next since to see only what changed after this call. It is the broker’s time, so never compare it with your own clock.
Consumer supervision
Every SDK can report what its consumers are doing (workers, handler progress) to the dashboard, through the KV API. It is off by default; turn it on per consume loop with .supervision({ group: 'billing-production' }), spelled as above in each language. Each run of a loop is one instance with fresh counters, published every 10 seconds (PHP: when the loop gets control back). The status contract defines what is published.
The broker inside a Rust program
A Rust program can run the broker in its own process, as the library queen::Broker. Every call
goes through the same handler functions the HTTP router dispatches to, so behaviour and defaults are
the broker’s, and the data lives in a directory on local disk that you name.
2.0 is not on crates.io yet (the queen-engine published there is 1.3, which ran on PostgreSQL),
so depend on the repository, pinned to the 2.0.0 commit:
[dependencies]
queen-engine = { git = "https://github.com/queen-mq/queen", rev = "86562c25d", default-features = false }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
serde_json = "1"The crate imports as queen and needs Rust 1.88. default-features = false leaves out what only
the standalone binary needs: the HTTP listener, the dashboard, the embedded proxy and the Kafka
facade. Start it on a data directory:
let broker = Broker::start(BrokerConfig::new().raft(&dir))
.await
.expect("broker start");Push, with a transactionId on the first item:
let first = broker
.push(vec![
qp::PushItem::new(q.clone(), serde_json::json!({"n": 1}))
.transaction_id(txn_id.clone()),
qp::PushItem::new(q.clone(), serde_json::json!({"n": 2})),
qp::PushItem::new(q.clone(), serde_json::json!({"n": 3})),
])
.await
.expect("push");Ack one message and push the next in one transaction:
let txn = broker
.transaction(
&qp::TransactionRequest::new(vec![
qp::TxnOperation::Ack(qp::TxnAckOperation {
transaction_id: m0.transaction_id.clone(),
partition_id: m0.partition_id.clone(),
status: qp::AckStatus::Completed,
consumer_group: Some(group.to_string()),
lease_id: Some(m0.lease_id.clone()),
error: None,
}),
qp::TxnOperation::Push {
items: vec![qp::TxnPushItem::new(
q2.clone(),
serde_json::json!({"stage": 2}),
)],
},
])
.with_required_leases([m0.lease_id.clone()]),
)
.await
.expect("transaction");
assert!(txn.success, "transaction must commit: {txn:?}");The embedded broker is one node and one tenant. It exposes push, pop, ack, lease renewal, transactions, queue configuration, the dead-letter queue and ephemeral queues. KV and timers are reachable only inside a transaction, so there is no standalone KV read or timer listing, and consumer-group administration, queue listings, traces and streams are not exposed. A panic in one of the broker’s core threads (planner, log writer, apply, checkpoint, raft) aborts the process, and the next start replays from the data directory; any other panic, your own included, unwinds as usual. Never open one data directory from two brokers at once.