The PyPI package is queen-mq; the import name is queen. It is fully async, built on httpx,
and ships a py.typed marker. Version 1.3.0, aligned with the broker’s 1.3 line.
from queen import Queen, Admin, Stream, token_bucket_gate, sliding_window_gateThe package also exports the typed dicts Message, AckResponse, BufferStats, DLQResponse,
TransactionResponse, and the default dicts CLIENT_DEFAULTS, QUEUE_DEFAULTS,
CONSUME_DEFAULTS, POP_DEFAULTS, BUFFER_DEFAULTS.
Constructor
Queen(config=None, *, urls=None, url=None, timeout_millis=None, retry_attempts=None,
retry_delay_millis=None, load_balancing_strategy=None, affinity_hash_ring=None,
enable_failover=None, health_retry_after_millis=None, bearer_token=None,
headers=None, retry_429=None, transport=None)config may be a URL string, a list of URL strings, or a dict. Keyword arguments override
whatever config supplied; a None keyword is ignored rather than treated as an override. URLs
are validated. Neither url nor urls raises ValueError("Must provide urls or url in configuration").
| Argument | Default | Effect |
|---|---|---|
url |
none | Single broker URL. |
urls |
none | Multiple brokers. More than one entry activates the load balancer. |
timeout_millis |
30000 |
Per-request deadline. |
retry_attempts |
3 |
Attempts for 5xx and network failures. |
retry_delay_millis |
1000 |
First retry delay; doubles per attempt. |
load_balancing_strategy |
"affinity" |
"affinity", "round-robin" or "session". |
affinity_hash_ring |
128 |
Virtual nodes per backend on the hash ring. |
enable_failover |
True |
On a 5xx or network error, try the next backend. |
health_retry_after_millis |
5000 |
How long a backend stays marked unhealthy. |
bearer_token |
None |
Sent as Authorization: Bearer <token>. |
headers |
{} |
Extra headers on every request. |
retry_429 |
None |
HTTP 429 backoff policy. See below. |
transport |
None |
httpx.BaseTransport override, e.g. httpx.MockTransport, so tests need no broker. |
The load-balancing arguments are inert with a single URL: the LoadBalancer is only constructed
when urls has more than one entry.
retry_attempts and failover are mutually exclusive paths. With one URL, or with
enable_failover=False, a call makes up to retry_attempts attempts against the same backend
(3 by default). With several URLs and failover on, retry_attempts is not consulted at all: the
call walks the backend list, trying each at most once.
retry_429
retry_429 is a plain dict (see queen.types.Retry429Config) with max_attempts, base_ms
and cap_ms, separate from retry_attempts; the defaults, the Retry-After contract and the
jitter are in
What the clients do with a 429.
Queen
| Member | Signature | Returns |
|---|---|---|
queue |
queue(name=None) |
QueueBuilder |
admin |
property | Admin, lazily created, one per client |
transaction |
transaction() |
TransactionBuilder |
ack |
async ack(message, status=True, context=None) |
dict (see below) |
renew |
async renew(message_or_lease_id) |
one dict for a scalar input, a list for a list input |
flush_all_buffers |
async flush_all_buffers() |
None |
get_buffer_stats |
get_buffer_stats() |
{"activeBuffers", "totalBufferedMessages", "oldestBufferAge", "flushesPerformed"} |
delete_consumer_group |
async delete_consumer_group(consumer_group, delete_metadata=True) |
broker body, or {"success": True} on a 204 |
update_consumer_group_timestamp |
async update_consumer_group_timestamp(consumer_group, timestamp) |
broker body, or {"success": True} |
close |
async close() |
None. Flushes buffers, cleans up, closes the HTTP client, removes signal handlers |
Queen is an async context manager: async with Queen(url) as client: closes on exit.
ack(message, status, context)
message is a message dict, a transaction-id string, or a list of either. status is True →
completed, False → failed, or a status string passed through verbatim. context accepts
{"group": ..., "error": ...}.
partitionId is mandatory on every message dict. Without it a single ack returns
{"success": False, "error": "Message must have partitionId property to ensure message uniqueness"}
and a batch raises ValueError. A leaseId on the message is forwarded when present. Per-message
statuses inside a batch work by tagging items with _status and _error; if any item carries
either key the whole batch switches to per-item mode.
Return shapes differ between the two paths:
| Call | Success | Rejection |
|---|---|---|
| single | {"success": True, **first_result} |
{"success": False, "error": ...} |
| batch | {"success": True, "results": <broker array>} |
{"success": False, "error": ...} |
| empty list | {"processed": 0, "results": []} |
none |
renew(message_or_lease_id)
Accepts a lease-id string, a message dict, or a list of either. Lease ids are deduplicated in
insertion order: with multi-partition pop, every message in a batch shares one leaseId and one
extend call renews every claimed partition, so passing the whole list issues one HTTP call.
QueueBuilder
client.queue(name) returns a fresh builder. Configuration methods return self; terminal
methods are push(), pop(), consume(), create(), delete(), dlq() and flush_buffer().
Addressing
| Method | Default | Notes |
|---|---|---|
partition(name) |
"Default" |
The ordered lane. Anything other than Default switches pop to the partition-scoped route. |
namespace(name) |
None |
Grouping label at create time; also a pop filter. |
task(name) |
None |
Second grouping label; same dual role. |
group(name) |
None |
Consumer group. Absent, the broker uses __QUEUE_MODE__. |
Queue lifecycle
| Method | Wire call | Returns |
|---|---|---|
config(options) |
none | self. Merges options over QUEUE_DEFAULTS. |
create() |
POST /api/v1/configure |
OperationBuilder |
delete() |
DELETE /api/v1/resources/queues/:queue |
OperationBuilder; raises ValueError with no queue name |
QUEUE_DEFAULTS carries the same nine options as the JavaScript and PHP clients, with the
values listed under Queue option defaults, as
snake_case keys (lease_time, retry_limit, priority, delayed_processing,
window_buffer, max_size, retention_seconds, completed_retention_seconds,
encryption_enabled) that the builder converts to the wire’s camelCase before sending.
Any extra key you pass to config() is converted the same way and sent, so options absent from
the defaults dict are still reachable.
Since 1.6.0 /configure merges: an option the body does not carry
keeps the value the queue already has, and only "mode": "replace" resets what the body omits.
This client sends no mode, so create() always merges. Note what that leaves: because
config() merges over QUEUE_DEFAULTS, those nine keys are on the wire whether you named them
or not and land on the client’s default rather than on the queue’s stored value. The twelve
options outside that dict, dedup_window_seconds and retention_enabled among them, are the
ones a partial call now leaves alone.
Producing
| Method | Default | Notes |
|---|---|---|
buffer(options) |
off | {"message_count", "time_millis", "max_size", "retry_delay_millis"}, defaults 100 / 1000 / 4 * message_count / 250. Turns push() into a bounded enqueue. |
push(payload) |
none | Returns a PushBuilder; payload is one dict or a list of dicts. Raises ValueError with no queue name. |
Each item may carry data, payload, or be used as the payload itself. The client mints a
UUIDv7 transactionId when the item does not supply one, and sends traceId only when it is
a valid UUID.
The broker’s push path stores no trace id, so it is dropped and the message pops back
with traceId: null. Push inside a transaction when a trace id has to survive. See
Pick your SDK.
res = await client.queue("orders").partition("customer-42").push([
{"data": {"orderId": 9137, "amount": 99.5}}
])PushBuilder
Awaitable: await executes the push. on_success(cb), on_error(cb) and on_duplicate(cb)
register callbacks and return the builder. Awaiting a non-buffered push resolves to the broker’s
per-item array; a buffered push resolves to a buffered marker once the buffer has accepted the
messages, without sending anything. While the buffer sits at max_size the await parks until
the flusher drains below the bound, so under overload the producer degrades to the flush rate
instead of growing the heap.
Consuming
| Method | pop() default |
consume() default |
Notes |
|---|---|---|---|
batch(size) |
broker-sized | broker-sized | Total message budget for the call, shared across every partition it claims. Left unset the broker sizes it (pop autopilot); the pre-1.2 client default of 1 comes back with autopilot off. batch(0) reads as unset. |
partitions(n) |
broker-sized | broker-sized | Claim up to N partitions per call; all share one leaseId. Left unset the broker sizes it (pop autopilot). Sent verbatim when set, 1 included, because a pinned width is a decision the broker must not widen; with autopilot off it is sent only when > 1. |
autopilot(enabled) |
True |
True |
Broker-side pop sizing for the knobs above that you did not set. On by default; false restores the pre-1.2 client defaults (batch 1, partitions 1) and sends no autopilot parameter. QUEEN_SDK_POP_AUTOPILOT=off does the same for a whole process, read once when the client is built. Setting both knobs leaves nothing to decide, so no parameter is sent then either. See pop autopilot. Requires broker >= 1.2; an older one applies its own defaults (batch 200, partitions 1) to the omitted knobs, which is a sizing difference and nothing more. |
wait(enabled) |
False |
True |
Long-poll. |
timeout_millis(ms) |
30000 |
30000 |
Server-side long-poll deadline. The HTTP call adds 5 s of slack so the server times out first. |
auto_ack(enabled) |
False |
True |
The two meanings differ. See the warning. |
subscription_mode(mode) |
None |
None |
all | new. |
subscription_from(from_) |
None |
None |
"now" or an ISO timestamp. |
conflation(enabled=True) |
False |
False |
Last-value delivery: the pop returns only each partition’s newest visible message and retires the rest. Sent only when true. A property of the consumer GROUP, stored on its first registration; see conflation. Requires broker >= 1.1.0; against an older one the call raises rather than draining the backlog quietly. |
concurrency(count) |
n/a | 1 |
consume() only. Worker tasks. Clamped to ≥ 1. |
limit(count) |
n/a | None |
consume() only. Stop after N messages per worker. |
idle_millis(ms) |
n/a | None |
consume() only. Stop after N ms without a message. |
renew_lease(enabled, interval_millis=None) |
n/a | False |
consume() only. |
each() |
n/a | batch mode | consume() only. One message per handler call. |
pop()
async pop() → list[dict], [] when there is nothing to return.
messages = await client.queue("orders").batch(10).wait(True).pop()Route selection: queue plus non-Default partition uses
/api/v1/pop/queue/:queue/partition/:partition; queue alone uses /api/v1/pop/queue/:queue;
namespace or task without a queue uses /api/v1/pop. None of the three raises ValueError.
pop_result()
async pop_result() → PopResult(messages, autopilot), a named tuple. It is the same call as pop() and returns the messages plus the broker’s account of how it sized the claim: partitions, batch, and an optional wait_millis pacing hint the consume loop honours in place of its own delay between empty polls. autopilot is None when the pop did not engage autopilot, when the broker is older than 1.2, or when the answer was a bodiless 204, which has no body to carry an echo. See pop autopilot.
consume(handler, *, signal=None)
Returns a ConsumeBuilder, awaitable. signal is an asyncio.Event; setting it stops every
worker at the top of its next iteration. on_success(cb) and on_error(cb) register callbacks.
await client.queue("orders").group("workers").batch(20).each().consume(handle)Worker loop behaviour:
- A handler exception with
auto_ackon nacks the message and does not re-raise, so the worker continues. - In
each()mode a nack abandons the rest of the popped batch: the nack released the lease and clamped the broker cursor, so the remainder will be redelivered anyway. - A 429 that escapes the HTTP layer’s own retry sleeps for
Retry-After(or 1 s) and continues. - A network error sleeps and continues.
- A 403 stops the worker and re-raises.
message["trace"]
Messages delivered through consume() gain a trace coroutine that posts to /api/v1/traces:
await msg["trace"]({"traceName": ["tenant-acme"], "eventType": "info", "data": {"step": 1}})data is required, traceName accepts a string or list of strings, eventType defaults to
"info". It never raises; failures return {"success": False, "error": ...}.
Buffering
| Method | Notes |
|---|---|
buffer(options) |
Enable per-queue/partition buffering for this builder’s pushes. |
flush_buffer() |
async. Flush this builder’s buffer. Raises ValueError with no queue name. |
A buffer flushes at message_count messages or time_millis after its first message, whichever
comes first. Buffers are keyed on "<queue>/<partition>", so builders addressing the same queue
and partition share one.
The buffer is bounded and lossless under errors. At max_size waiting messages (default
4 * message_count), further adds await the flusher instead of growing the heap. A batch whose
POST fails goes back to the front of the buffer, in order, and is retried every
retry_delay_millis until it lands or the client closes; it is never dropped. A broker outage
therefore shows up as parked producers and a full buffer, not as silent loss.
Dead-letter queue
dlq(consumer_group=None) returns a DLQBuilder. Raises ValueError with no queue name.
| Method | Default |
|---|---|
limit(count) |
100 |
offset(count) |
0 |
from_(timestamp) |
none (trailing underscore, from is a keyword) |
to(timestamp) |
none |
get() |
async → {"messages": [...], "total": n} |
Read-only. Replay lives on the admin façade: admin.retry_message(partition_id, transaction_id).
TransactionBuilder
client.transaction(). Pushes and acks in one PostgreSQL transaction, all-or-nothing.
| Method | Notes |
|---|---|
ack(messages, status="completed", context=None) |
One message or a list. Requires transactionId and partitionId; a leaseId is collected into requiredLeases. Returns self. |
queue(queue_name) |
Returns a TransactionQueueBuilder with .partition(key) and .push(items); push() returns the parent builder. |
commit() |
async. Raises on an empty bundle and on a non-success response. |
await (client.transaction()
.ack(msg)
.queue("orders.enriched").push([{"data": {"id": 1}}])
.commit())A transaction is atomic, not exactly-once end to end; see the limit.
Admin
client.admin (property) or Admin(http_client). Every method is async and returns the
broker’s JSON body verbatim. Methods taking **params build a query string, dropping None.
Resources
| Method | Route |
|---|---|
get_overview() |
GET /api/v1/resources/overview |
get_namespaces() |
GET /api/v1/resources/namespaces |
get_tasks() |
GET /api/v1/resources/tasks |
list_queues(**params) |
GET /api/v1/resources/queues |
get_queue(name) |
GET /api/v1/resources/queues/:name |
Messages and traces
| Method | Route |
|---|---|
list_messages(**params) |
GET /api/v1/messages |
get_message(partition_id, transaction_id) |
GET /api/v1/messages/:partitionId/:transactionId |
delete_message(partition_id, transaction_id) |
DELETE /api/v1/messages/:partitionId/:transactionId |
retry_message(partition_id, transaction_id) |
POST /api/v1/messages/:partitionId/:transactionId/retry |
get_trace_names(**params) |
GET /api/v1/traces/names |
get_traces_by_name(trace_name, **params) |
GET /api/v1/traces/by-name/:traceName |
get_traces_for_message(partition_id, transaction_id) |
GET /api/v1/traces/:partitionId/:transactionId |
Status and analytics
| Method | Route |
|---|---|
get_status(**params) |
GET /api/v1/status |
get_queue_stats(**params) |
GET /api/v1/status/queues |
get_queue_detail(name, **params) |
GET /api/v1/status/queues/:name |
get_analytics(**params) |
GET /api/v1/status/analytics |
get_system_metrics(**params) |
GET /api/v1/analytics/system-metrics |
get_worker_metrics(**params) |
GET /api/v1/analytics/worker-metrics |
get_postgres_stats() |
GET /api/v1/analytics/postgres-stats |
Consumer groups
| Method | Route |
|---|---|
list_consumer_groups() |
GET /api/v1/consumer-groups |
get_consumer_group(name) |
GET /api/v1/consumer-groups/:name |
get_lagging_consumers(min_lag_seconds=60) |
GET /api/v1/consumer-groups/lagging?minLagSeconds= |
delete_consumer_group_for_queue(consumer_group, queue_name, delete_metadata=True) |
DELETE /api/v1/consumer-groups/:group/queues/:queue |
seek_consumer_group(consumer_group, queue_name, options) |
POST /api/v1/consumer-groups/:group/queues/:queue/seek |
refresh_consumer_stats() |
POST /api/v1/stats/refresh |
seek_consumer_group posts options verbatim: {"toEnd": True} or {"timestamp": "<RFC3339>"}.
System
| Method | Route |
|---|---|
health() |
GET /health |
metrics() |
GET /metrics |
get_maintenance_mode() |
GET /api/v1/system/maintenance |
set_maintenance_mode(enabled) |
POST /api/v1/system/maintenance |
get_pop_maintenance_mode() |
GET /api/v1/system/maintenance/pop |
set_pop_maintenance_mode(enabled) |
POST /api/v1/system/maintenance/pop |
Streaming SDK
Stream builds an immutable operator chain over a queue and runs it against /streams/v1/*.
Every combinator returns a new Stream.
Stream.from_(client.queue("orders"), **options) # or Stream.from_queue(...)| Group | Methods |
|---|---|
| Stateless | map(fn), filter(predicate), flat_map(fn) |
| Keying | key_by(fn) (at most one, must precede window / reduce / gate) |
| Windows | window_tumbling(**opts), window_sliding(**opts), window_session(**opts), window_cron(**opts) (at most one) |
| Reduce | reduce(fn, initial=None), aggregate(extractors) (require a preceding window; at most one) |
| Gate | gate(fn) (at most one, incompatible with windowing and reduce) |
| Sink | to(sink_queue_builder, partition=None), foreach(fn) (must be last in the chain) |
| Terminal | run(query_id, url, **opts) |
Window keyword arguments mirror the JavaScript names in snake_case (grace_period,
idle_flush_ms, event_time, allowed_lateness, on_late) and are converted before they reach
the wire, so the defaults and the close semantics in the
JavaScript window option table apply here unchanged.
token_bucket_gate(...) and sliding_window_gate(...) return ready-made gate functions.
run(query_id, url, **opts)
async, returns a Runner. query_id and url are positional-or-keyword and both required.
An empty query_id raises ValueError("run requires query_id=...").
| Option | Default |
|---|---|
batch_size |
200 |
max_partitions |
4 |
max_wait_millis |
1000 |
subscription_mode |
None ('all' | 'new') |
subscription_from |
None (ISO timestamp or 'now') |
conflation |
False. Requests last-value delivery on the source pop; the reducer only ever sees each partition’s newest message, so messages conflation skips never reach it. See conflation |
consumer_group |
streams.<query_id> |
reset |
False (wipe state on a config-hash mismatch) |
on_error |
None (callable(err, ctx)) |
bearer_token |
None |
logger |
None |
Options are passed as snake_case and mapped onto the runner’s camelCase keys internally. The
chain shape is fingerprinted into a config hash; re-deploying a different chain under the same
query_id is rejected at registration unless reset=True. Only operator kinds and their
structural config are hashed, not the bodies of your functions.
Runner exposes await stop() and metrics().
Logging
Off unless QUEEN_CLIENT_LOG=true is set in the environment. Levels are emitted through the
module logger in queen.utils.logger.
Errors
HTTP failures raise httpx exceptions; the client attaches the parsed body’s code to the
exception when present, with the proxy’s stable values listed under
Behind the proxy. A 204 response yields None:
a 204 carries no body at all.
Same surface, other languages
Parity here is a claim about the surface, not about shared code: each client is written natively
in its own idiom. These sixteen rows are that surface, read out of clients/ on this tree. Six
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 |
| 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 seven operations | yes | yes | yes | yes | yes | five of seven |
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 |
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 | no |
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 seven operations are
get,getMany,getPrefix,put,putIfAbsent,deleteandincr. C++ wraps five 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.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. - Lease renewal. In C++,
renew_lease()sets a flag the consumer loop does not act on: the worker carries an unimplemented placeholder where the timer would go. Callclient.renew(message)yourself before the lease expires. - 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.
The py suite in the test matrix runs the whole pytest tree
under clients/client-py/tests, streams included, on the single, ha and tenanted
topologies.