Skip to content

System and internal routes

Maintenance mode, the kv, timer and ephemeral kill switches, shared state, the stats refresh trigger and the broker-to-broker /internal routes: all admin-gated operator surfaces.

Updated View as Markdown

Everything on this page is an operator surface, not part of the client API. No SDK calls these routes, and a tenant on a shared cell must never reach them.

route_access_level maps every path starting with /api/v1/system/ or /internal/ to admin, plus /api/v1/stats/refresh explicitly. That mapping only protects anything when JWT is on.

None of these routes are tenant-scoped, with one exception: POST /internal/api/notify resolves a tenant, because the wake gate it triggers is keyed by (tenant, queue). Maintenance mode is a property of the process and the cell, not of a tenant.

Maintenance mode

Two independent flags, each with a GET and a POST. Both are stored twice: as an in-process AtomicBool (the source of truth for hot-path checks) and as a row in queen.system_state keyed maintenance_mode / pop_maintenance_mode with the value {"enabled": bool} (a best-effort mirror for restart and cluster propagation).

GET /api/v1/system/maintenance

Reads both flags fresh from queen.system_state, so a change made on another replica is reflected immediately, and updates the local atomics to match. If the pool or the query fails it falls back to the in-process atomics rather than returning an error: this route never 500s.

{
  "maintenanceMode": false,
  "popMaintenanceMode": false,
  "bufferedMessages": 0,
  "bufferHealthy": true,
  "bufferStats": {
    "pendingCount": 0,
    "failedCount": 0,
    "dbHealthy": true,
    "failedFiles": { "count": 0, "totalBytes": 0, "totalMB": 0.0, "failoverCount": 0, "qos0Count": 0 }
  }
}

bufferStats.failedFiles is read off the spool directory: count and totalBytes are the finalized .buf files still waiting to drain. failoverCount mirrors count; qos0Count is always 0.

POST /api/v1/system/maintenance

Body: enabled (boolean, required). A missing field is a 400 (enabled (boolean) is required).

{ "enabled": true }

What enabling it does, in order: sets the in-process flag, mirrors it to queen.system_state, pauses the background spool drain so buffered pushes accumulate, and broadcasts a MAINTENANCE_MODE_SET frame to mesh peers. Disabling it force-finalizes the active spool file and resumes the drain, so the spool replays into PostgreSQL.

{
  "maintenanceMode": true,
  "bufferedMessages": 0,
  "bufferHealthy": true,
  "message": "Maintenance mode ENABLED. All PUSHes routing to file buffer."
}

The message strings are exact and stable (Maintenance mode DISABLED. Background processor will drain buffer to DB. on disable) because tooling greps for them.

Effect on pushes. While the flag is on, POST /api/v1/push never touches PostgreSQL. Every item is written to the disk spool and reported with status: "buffered" at HTTP 201; an item whose spool write failed reports status: "failed" and the whole response becomes 500. A push that omitted transactionId gets one minted at buffer time, so the buffered result and the replay dedup key are both well defined.

Effect on a dead-letter replay. The two replay routes are refused instead, with 503 and result: "maintenance". A replay is a move, and a move cannot be spooled: the spool carries frames, not the deletion of the dead-letter row, so a spooled one would land in exactly the pushed-but-still-dead-lettered state the move primitive exists to make impossible. Nothing is lost by refusing, because the dead-letter row stays where it is and the same replay works once the switch is off.

GET /api/v1/system/maintenance/pop

{
  "popMaintenanceMode": false,
  "message": "Pop maintenance mode is OFF. Normal operation."
}

Unlike the push-maintenance GET, this one reads only the in-process flag. On a multi-replica deployment it therefore answers for the replica you happened to reach; GET /api/v1/system/maintenance is the route that re-reads the database.

POST /api/v1/system/maintenance/pop

Body: enabled (boolean, required).

{
  "popMaintenanceMode": true,
  "message": "Pop maintenance mode ENABLED. All POP operations will return empty arrays."
}

Effect on pops. Every pop route returns HTTP 204 immediately, before touching the database. The 204 carries no body at all (not even the paused flag the handler constructs), because announcing a content length on a body that hyper then elides poisoned strict HTTP/1.1 clients under empty-poll load. Clients treat 204 as “no messages” and retry, so a paused broker looks like an idle one to a consumer. That is deliberate, and it is why the flag has an explicit GET.

The flip is mirrored to queen.system_state and broadcast to mesh peers as POP_MAINTENANCE_MODE_SET.

The kv and timer kill switches

Access level admin, like every route under /api/v1/system/.

KV and timers have no boot flag. There is no variable that decides whether those routes exist, for the same reason there is none for push and for pop, so nothing here is a way to opt into a feature. What these three switches are is the other instrument entirely: “stop the bleeding, now”, for an incident at three in the morning that a rollout does not resolve. They are on the same pattern as maintenance mode above, an in-process atomic that is authoritative on the hot path, mirrored into queen.system_state for propagation and restart.

That is why a paused surface answers 503 with Retry-After and never 404: an operator pulled a lever, it is temporary, and the client should come back. A 404 on a KV or timer path means the path is wrong. Inside the transaction wire the same pause answers 403, because a bundle that retries a paused surface forever is a client spinning on the hot path with messages in hand.

GET /api/v1/system/kv-timers

{
  "kvEnabled": true,
  "timersScheduleEnabled": true,
  "timersFireEnabled": true,
  "quotaTenants": 12,
  "quotaAgeMs": 8412,
  "quotaHot": 1
}

The three …Enabled fields are the switches, and they are what POST writes. There is no boot half to report beside them any more. The GET re-reads the mirror rows before answering and falls back to the in-process atomics, so it never fails on a busy pool.

quotaAgeMs is the number to look at when limits behave oddly. It is how stale the measurement the quota gate is enforcing against has become, and a value that keeps growing means the refresh is failing. Enforcement still works, because each broker adds its own local delta to whatever it last measured, but nothing is being released: a tenant that has come back under its limit stays blocked. quotaHot counts the tenants above the hot threshold, whose measurement is refreshed on the faster cadence.

POST /api/v1/system/kv-timers

{ "timersSchedule": false }
Field Effect when false
kv every KV route answers 503, and the kv array of a transaction is refused permanently. Nothing already stored is affected
timersSchedule no new timers are accepted. The ones already scheduled still fire, and cancels are never blocked
timersFire promised messages are not being delivered

Each field is independent and an omitted one is left alone. A body that reset the unmentioned ones would turn “pause new timers” into “and also stop delivering the ones already promised”. A body with none of the three is a 400.

An absent mirror row means on. That is the difference between a kill switch and a feature flag, and getting it backwards would mean every fresh cell booting with the feature dead and no row to explain why.

Every flip of any of the three writes a single sweeper log line at the instant it happens, naming the switch and what the operator has just done to the cell. It is on-change only, so a switch that has been off for an hour does not write an hour of lines.

The ephemeral kill switch

Same instrument, one switch, for the ephemeral route family. It is on the same pattern as the three above: an in-process atomic mirrored into queen.system_state, an absent row meaning on, and a paused surface answering 503 with Retry-After rather than 404.

GET /api/v1/system/ephemeral

{
  "ephemeralEnabled": true,
  "cellBytes": 8402944,
  "cellMaxBytes": 268435456,
  "queues": 314,
  "grantedTenants": 12,
  "requireGrant": true,
  "epoch": "9f3a1c"
}

Every number here is an in-process gauge: this route reads no table and counts nothing, which is what makes it safe to poll during the incident it exists for. cellBytes against cellMaxBytes is the pair a 503 ephemeral_unavailable is decided on. grantedTenants at 0 with requireGrant true means every tenant is being refused, which is the single most confusing state to diagnose from the outside. epoch is this broker’s incarnation id, the one carried inside every ephemeral message id, so a support ticket about an ack that answered stale can be resolved against it.

POST /api/v1/system/ephemeral

{ "enabled": false }

With enabled: false every /api/v1/ephemeral route answers 503. Consumers already parked stay parked until their own timeout expires, the rings keep their memory until the idle collector reaches them, and nothing stored durably is affected, because nothing of this class is stored durably except the declarations. Turning it back on resumes service on rings that are, by then, whatever the age and bound limits have left in them.

GET /api/v1/system/shared-state

Returns a hard-coded single-node summary:

{
  "enabled": false,
  "reason": "single_node_segments_broker",
  "maintenance_mode": false,
  "pop_maintenance_mode": false
}

POST /api/v1/stats/refresh

Access level admin. Runs queen.log_refresh_all_stats_v1() (the same reconciler the broker’s own loop runs every STATS_INTERVAL_MS, scheduled through the stats_refresh claim row in queen.maintenance_leases) and returns its summary JSON verbatim, labelled engine: "segments" with a segPartitions count. The route does not touch the schedule: forcing a refresh never delays or doubles the background cadence.

Use it to force a refresh instead of waiting out the interval. If the dashboard’s numbers are stale rather than merely late, check whether that loop is running at all (a large statsAge on the status routes). It refreshes the counters only: retainedBytes is owned by the retained-bytes lane (RETAINED_BYTES_INTERVAL_MS, advisory lock 737003) and keeps its last value here.

Internal broker-to-broker routes

Three routes exist for the mesh layer. They are admin-gated and are not a public contract: treat their shapes as version-coupled to the broker.

POST /internal/api/notify

Body: queue (required, non-empty), partition (optional). A missing queue is a 400.

{ "queue": "orders", "partition": "eu-1" }

Emits exactly the signal a local push emits: wakes this replica’s parked long-poll pops for that (tenant, queue) and fans the signal out to mesh peers. Returns {"status": "ok"}. This is the HTTP fallback for a peer that has no mesh reachability, and the hook for an external system that wrote to the database out of band and wants parked consumers woken now instead of at the next poll interval.

The tenant comes from the same x-queen-tenant header every other route uses; absent, it is the default tenant. The header is unauthenticated by design, and the trust boundary for it is the proxy, which is another reason this path must not be reachable from outside the cell.

GET /internal/api/shared-state/stats

GET /internal/api/inter-instance/stats

Identical bodies; the second is a legacy alias. With a mesh transport running:

{
  "server_id": "queen-0",
  "transport": "tcp-mesh",
  "port": 6633,
  "peer_count": 1,
  "peers": [
    { "host": "queen-1", "port": 6633, "connected": true, "resolved": true, "resolved_ip": "10.0.0.12" }
  ],
  "servers_alive": 1,
  "servers_dead": 0,
  "eph_members": 1,
  "messages_sent": 41022,
  "messages_received": 40911,
  "messages_dropped": 0,
  "signature_failures": 0,
  "handshake_failures": 0,
  "sequence_rejections": 0,
  "enabled": true,
  "running": true,
  "maintenance_mode": false,
  "pop_maintenance_mode": false
}

With no peers configured, or no transport:

{
  "enabled": false,
  "reason": "no_peers",
  "maintenance_mode": false,
  "pop_maintenance_mode": false
}

signature_failures and handshake_failures are the same counter under two names. sequence_rejections is always 0: there is no sequence tracking. eph_members counts the live peers that advertised the v2 handshake and therefore participate in the ephemeral rendezvous ring; during a rolling deploy it sits below servers_alive, which is the expected reading, not an error.

Firewall the mesh port

The mesh is framed TCP on QUEEN_MESH_PORT (default 6633), dialled from QUEEN_MESH_PEERS (the older QUEEN_UDP_* variable names survive as aliases, which is why the default port comes from QUEEN_UDP_NOTIFY_PORT when the new name is unset). The listener binds QUEEN_MESH_BIND_ADDR, defaulting to the HTTP listener’s QUEEN_BIND_ADDR. Its HELLO handshake is HMAC’d, but the nonce is generated by the dialer and never tracked, so a captured handshake is replayable. After the handshake, frames are unauthenticated JSON. The frame set includes MAINTENANCE_MODE_SET and POP_MAINTENANCE_MODE_SET.

Anyone who can open a TCP connection to that port can therefore stop the cell from accepting pushes or delivering messages, without any token. Restricting the mesh port to the broker replicas themselves is a requirement, not a hardening tip.

No migration routes

There is no /api/v1/migration/* surface: those routes were removed. Backing up Queen is a plain pg_dump of the database, and the offline rows-to-segments conversion is a CLI subcommand of the binary, not an HTTP endpoint.

Access levels

Route Level
GET /api/v1/system/maintenance admin
POST /api/v1/system/maintenance admin
GET /api/v1/system/maintenance/pop admin
POST /api/v1/system/maintenance/pop admin
GET /api/v1/system/shared-state admin
POST /api/v1/stats/refresh admin
POST /internal/api/notify admin
GET /internal/api/shared-state/stats admin
GET /internal/api/inter-instance/stats admin

Deployment guidance for these surfaces (what to expose, where to terminate TLS, how the proxy classifies routes) is in the self-hosting section.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close