Skip to content

Client SDKs in six languages: install and connect

Install and connect the Queen client for JavaScript, Python, Go, Rust, PHP or C++: the package, the minimum versions, how each SDK names things, and what each one lacks.

Updated View as Markdown

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.

Why
the logic lives in the broker, so a client has little to get wrong, and a seventh language is a few hundred lines of HTTP calls.

Install and connect

npm install queen-mq
import { 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-mq
from 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/v2
import 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,macros
use 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 -lpthread

C++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-client
use 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:6632

See 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, incr and check. C++ wraps six of them: getMany and getPrefix have no method, and both remain reachable through POST /api/v1/kv on the shared HttpClient. getPrefix inside a transaction is refused by the broker in every client, which is its rule and not a client’s gap.
  • Timers. C++ has schedule and cancel only; peek and list are reads and stay on GET /api/v1/timers/{queue}[/{timerKey}]. There is no reschedule operation anywhere, because schedule is the upsert and status reports which happened; Python, Go, Rust and PHP still spell a reschedule alias over it, JavaScript and C++ do not.
  • once. JavaScript (kv.once, transaction().once) and Python (kv.once, transaction().once) fold putIfAbsent plus required into the question people actually ask, “did I win?”. The other four write the putIfAbsent themselves, 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.
  • kv on a streams operator context. No client has it, so it is not a row. Inside a stream the state primitive is state_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.
  • Admin facade. C++ has no Admin class. Every management and observability route is still reachable, through the shared HttpClient returned by get_http_client().
  • affinity. The C++ LoadBalancer implements round-robin and "session" only, and load_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 Stream builder, 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 manual pop() never carries it. Rust records a trace through Admin::record_trace instead 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.
  • Host override. 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.
  • pop failures. 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.

Why
a dashboard that shows which workers are alive and what they are doing saves a lot of log reading. It stays off by default because it writes to KV.

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:

server/tests/embedded_raft_smoke.rsrust
let broker = Broker::start(BrokerConfig::new().raft(&dir))
    .await
    .expect("broker start");

Push, with a transactionId on the first item:

server/tests/embedded_raft_smoke.rsrust
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:

server/tests/embedded_raft_smoke.rsrust
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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close