Queen is one stateless Rust binary (crate queen, binary queen) and one
PostgreSQL database. The binary holds no durable state. Every offset, cursor, lease and
segment lives in PostgreSQL, and every in-memory structure in the broker is a cache or a
scheduler over it. That single fact is the key to reading this section: when an in-memory
structure is stale, wrong, or lost to a restart, the outcome is extra work, not lost or
misdelivered messages. Each page below states its own version of that argument, because each
structure earns its place only by having one. The one deliberate exception is the
ephemeral queue class, whose contents live in broker memory by contract: only
the declared configuration is a PostgreSQL row, its hot verbs take neither an admission slot nor a
pooled connection, and losing its contents on a restart is that class’s documented trade, not a
defect. Everything else in this section is state PostgreSQL owns.
Read the concept pages first if you want to know what Queen promises. This section explains how it keeps those promises, and where it pays for them.
The request path
An HTTP request crosses the same five layers regardless of what it does.
- The axum router in
server/src/main.rs. Eighty-four method-and-path pairs on one flatRouter, plus a fallback that serves the dashboard SPA compiled into the binary withrust_embedfromserver/webapp/dist. There is no runtime static-directory override. There is no TLS listener and no CORS layer: the broker speaks plaintext HTTP onPORT(6632) and expects a proxy in front of it. - Middleware. JWT validation (off by default) resolves an access level and an
authenticated
sub; tenant resolution readsx-queen-tenantwhenQUEEN_TENANCY_HEADER=1, otherwise stamps the fixed default tenant. Both run before any handler and both are transparent when disabled. - An admission slot. Every write transaction passes through one process-wide arbiter that sizes itself from PostgreSQL’s commit-flush pipeline, with four lanes (push, pop, ack, maint) sharing the budget. See Flow control.
- A pooled connection. One
deadpool-postgrespool,DB_POOL_SIZEconnections (default 160), shared by handlers and background jobs. Hot statements go throughprepare_cached, so a steady-state call re-uses a prepared statement on that connection. - One stored-procedure call. Almost every route is a single
SELECT queen.<sp>(...)whose JSON result the handler forwards, sometimes verbatim. The decision procedures (who gets which message, whether a push is a duplicate, what an ack commits) live in SQL, under row locks, not in Rust.
A pop that has to wait is the one exception worth knowing: it releases both the permit and the connection before it parks, and re-acquires them on wake. A parked long-poll costs the broker a timer and nothing else.
What lives in memory
Nothing in this table is durable. Every entry is followed by what its being wrong costs.
| Structure | Module | Replaces | Cost of being wrong |
|---|---|---|---|
| Push fusion shards | fusion.rs |
one transaction per push request | the bundle fails; participants are told error and spooled to disk |
| Hot-list rings | hotlist.rs |
the SQL wildcard candidate scan | one empty SKIP LOCKED claim that returns no rows |
| Dedup cache | dedup.rs |
nothing: it only narrows the SQL probe | the probe widens to the full window |
| Ack registry | ack_registry.rs |
per-ack hash resolution | the ack falls through to the SQL path |
| Ack fusion buffers | ack_fusion.rs |
one transaction per ack | the whole flush errors; leases expire and redeliver |
| Long-poll gates and hints | notify.rs |
polling | the parked pop waits out its own backoff |
| Disk spool | file_buffer.rs |
losing pushes during a database outage | pushes fail outright |
The two things that decide what data exists (the deduplication verdict and the consumer cursor) are not in this table. They are in PostgreSQL, computed under a row lock, on every call.
The storage side
Storage is the “log engine”: four tables created by 001_log_schema.sql (log_partitions,
log_segments, log_txns, log_consumers) plus queen.log_dlq from the ack file, all hanging
off queen.queues, which is both a queue’s identity and its configuration
(log_partitions.queue_id references it directly, ON DELETE CASCADE). A partition is an ordered
lane; a message’s position is a single monotone BIGINT offset; a segment is one row holding many
messages packed together and zstd-compressed. The storage model has the
row-by-row detail.
Two further tables hang off nothing at all. queen.kv and queen.log_timers are created at every
boot like the rest, and stay empty unless an operator switches their surfaces on. Neither names a
partition, deliberately, which is what keeps them out of the per-partition maintenance scan; see
KV internals and timer internals. The ephemeral class keeps
two more (queen.ephemeral_queues, queen.ephemeral_quota): declared configuration and quota
rows only, never message contents.
All of that DDL is include_str!-embedded in the binary and re-applied idempotently at every
boot, under an advisory lock and with no migrations, because the deployment model is
always-virgin; the schema catalogue has the apply order and its rules.
Background jobs
Several loops run alongside the HTTP server. Each is spawned before axum::serve, each
swallows its own errors so a transient database failure cannot kill it. The cluster-singleton loops
schedule through one durable claim row each in queen.maintenance_leases, so their cadence is the
configured value whatever the replica count; the advisory locks stay inside those cycles as belt
against old-image pods.
| Loop | Cadence | Leader gate | What it does |
|---|---|---|---|
| Retention | RETENTION_INTERVAL, 5000 ms |
claim row retention + transaction lock 737001 as belt |
bounded autocommitting delete steps (Retention) |
| Stats reconciler | STATS_INTERVAL_MS, 10000 ms |
claim row stats_refresh + transaction lock 737002 as belt |
recomputes queen.stats from the log tables (Stats) |
| Retained-bytes lane | RETAINED_BYTES_INTERVAL_MS, 600000 ms |
claim row retained_bytes + transaction lock 737003 as belt |
recomputes queen.stats.retained_bytes from live segments (Stats) |
| Metrics collector | METRICS_FLUSH_MS, 60000 ms |
none, by design | per-replica rows in worker_metrics and system_metrics |
| Hot-list wheel tick | 50 ms | none | promotes due deferred and lease-parked ring entries |
| Hot-list wake tick | 5 ms | none | one coalesced wake per queue that received pushes |
| Hot-list reseed floor | 2 s tick, per ring QUEEN_HOTLIST_RESEED_MS |
none | re-derives ring contents from PostgreSQL, bounded to recently written partitions |
| Hot-list full reseed | per ring, QUEEN_HOTLIST_RESEED_FULL_MS, 300000 ms |
none | the same over every partition of the queue, the floor for pendingness no write explains |
| Reconcile | QUEEN_CACHE_REFRESH_INTERVAL_MS, 60000 ms |
none | re-reads maintenance flags, drops per-queue caches, applies the hot-list repair markers a seek or a group delete published |
| Idle sweep | QUEEN_HOTLIST_IDLE_SWEEP_MS, 300000 ms |
none | drops rings and wake gates for queues nobody touched |
| Unserved-ring trim | QUEEN_HOTLIST_UNSERVED_TRIM_MS, 30000 ms |
none | drops rings this broker serves no pops for, the standby-broker bound on ring memory |
| Spool drain | FILE_BUFFER_FLUSH_MS, 100 ms |
none | replays disk-spooled pushes once the database is reachable |
| Sweeper | due-driven, 5 ms to 1000 ms | none, by design | fires due timers into the log and prunes expired KV rows (the sweeper) |
The metrics collector is deliberately un-gated: every replica records its own rows, keyed by hostname and worker id, and the dashboard aggregates across them.
The sweeper is un-gated for a stronger reason, and it is the opposite of retention’s. Any ownership
scheme orphans the work of a broker that dies, and an orphaned timer never fires, which is the worst
failure this component can have. So every replica drains in parallel and the sharing mechanism is
FOR UPDATE ... SKIP LOCKED rather than an election. It is spawned on every replica, because the two
surfaces it serves are on every replica; QUEEN_SWEEPER=false is the only thing that stops it.
The mesh
With QUEEN_MESH_PEERS set, replicas form a full mesh over framed TCP. It carries wakes for
parked pops when a push commits on another replica, hot-list pending marks, maintenance-mode
flips, queue-config cache invalidation, and the admin fan-out for ephemeral queues (whose data
verbs travel broker-to-broker over plain HTTP instead, to the owner the HELLO handshake
advertised). Nothing it carries is authoritative: PostgreSQL
is re-read every QUEEN_CACHE_REFRESH_INTERVAL_MS and a parked pop re-polls on its own
backoff, so a dropped frame costs latency only. It also has a security boundary you must
enforce yourself: see The mesh.
Working on Queen
The four pages under Contributing are the same tier pointed at the repository rather than
at the running system. They are the answer to “where does this claim come from”: the workspace
and the build order, the suites each page names in its verifiedBy frontmatter, the generators
that derive this site’s fact tables from server/, and what a release actually publishes.
Building and developing
The workspace, the build order that matters, and the compile-time embedding that makes a SQL edit invisible until you rebuild.
Testing
What each suite in the matrix covers, how to run one on its own, and what is green today.
The documentation site
The frontmatter contract every page here obeys, the generators behind the fact tables, and the checks that fail a build rather than let a page go stale.
How Queen ships
The registries a release touches, where the version number lives, and the one boundary a rollback cannot cross.
Where to go next
The storage model
Segments, offsets, the log tables and the queue row they hang off: what each row is, and the invariants that make offset arithmetic safe.
Life of a push
From HTTP body to committed rows: grouping, intra-request dedup, and how N partitions cost one commit.
Life of a pop
Candidate selection, the claim under FOR UPDATE SKIP LOCKED, the head probe, and lease versus auto-ack.
The hot-list
The in-memory ring that replaced the SQL candidate scan: tri-state verdicts, epoch-CAS clears, and the revisit wheel.
Deduplication
How exactness is achieved in SQL, and why the broker-side cache can never change a verdict.
Ack internals
The ack registry, ack fusion, and how a transactionId resolves to an offset through the hash sidecar.
Flow control
The adaptive concurrency limiter: what it measures, how the limit moves, and why raising the maximum does nothing.
Retention internals
Bounded autocommitting steps, one partition lock at a time, and the three eviction watermarks.
Counters, rates and lag
Where every number on the dashboard comes from, and why the manual refresh route does nothing useful.
The mesh
Framed TCP, the HMAC HELLO handshake, the frame set, and the security reality.
Timer internals
Why a scheduled message cannot live in the log, the staging table keyed by names, and the two lease columns.
KV internals
The identity with no partition id, a version that is not monotonic, and one predicate as the only definition of existence.
The sweeper
One component with two clocks, leaderless, and the livelock a claim-then-fire loop walks into.
Schema and procedure catalogue
Every log table with its columns, and every log-engine stored procedure with its signature and role.
Retired objects
The engines that came before, and why nothing of them survives: deployments start from an empty database.