Scaling and deployment¶
A deployment is PostgreSQL plus one or more worker processes. The CLI and web interface are optional.
Each worker serves one logical queue and advertises the (task_name, handler_version) pairs it can run. You scale by
adding workers or raising per-worker concurrency, and you isolate workloads by giving them separate queues.
Two connection paths¶
This is the deployment detail that surprises people, so it comes first.
| Path | Used for | Requirement |
|---|---|---|
| Pool | Short state transitions - claim, renew, complete | Any pooler, including transaction pooling |
| Listener | LISTEN for wake-ups |
A session-capable endpoint |
LISTEN registers interest on a specific backend session. A transaction-pooling proxy hands your next statement to a
different backend, so the registration is lost and notifications never arrive.
The symptom is not an error. It is a system that works, slowly, because every task waits for the reconciliation poll instead of the notification.
So: point the listener at a direct PostgreSQL endpoint or a PgBouncer session pool. One listener connection multiplexes every channel a process needs - queue, scheduler, wait, and result.
Sharded notifications¶
Ready and result notifications each use 64 deterministic channels.
A single global channel would wake every worker for every task, and each one would query, find nothing for itself, and go back to sleep. That cost grows with the product of workers and task rate.
With shards, a queue runtime receives only its queue's payload, and a result waiter only its task's payload. Wake-ups stay proportional to relevant work.
They remain hints. Every wake-up is followed by a state read, and bounded reconciliation polling covers disconnects and missed notifications.
Concurrency and overload¶
Two independent control loops manage load, and mixing them up leads to confusing incidents.
In-process admission control watches event-loop lag, lease-renewal age, and database failures. When it detects sustained overload it reduces the effective concurrency limit - which stops the worker claiming new work. It never cancels a running handler and never exceeds your configured concurrency. Recovery is additive, so a worker climbs back gradually rather than slamming the database again.
Kubernetes replica scaling adds or removes whole workers based on queue demand, using ready-task count or
oldest-ready age exposed by pgtask.queue_demand.
They are deliberately separate: one manages a single process against the database it can see, the other manages fleet size against a queue's backlog. Neither is part of task correctness. Turning both off changes throughput, not guarantees.
Do not scale schedulers to zero
If your autoscaler can remove every replica, make sure it cannot remove every scheduler-enabled worker. Schedules only advance while something is running to advance them.
Health endpoints¶
Workers expose pod-local /livez and /readyz. They answer different questions, and conflating them causes restart
loops.
/livezdepends only on supervisor progress. It answers "is this process wedged?" It stays healthy during a database outage, because restarting a worker does not fix PostgreSQL being down./readyzcovers claim admission, database connectivity, listener health, and lease-renewal health. It answers "should this worker receive work right now?"
Draining¶
On shutdown, a worker stops claiming and lets running handlers finish within the grace period. Handlers that do not finish are aborted, and their tasks return through lease expiry - which is the ordinary recovery path, not a special case.
A drain-only mode lets you keep a worker finishing in-flight work without accepting anything new.
What runs out first¶
In rough order:
- The listener path, if you route it through a transaction pooler. Everything works but latency is poll-bound.
- Connections. Workers × pool size adds up faster than expected against a default
max_connectionsof 100. - PostgreSQL write throughput. Every transition is a write; retention deletes are writes too.
- Table bloat on
tasks, if retention is too generous. The claim indexes are partial, which helps, and the table carries tuned autovacuum settings for its churn.
Capture PostgreSQL CPU, WAL rate, lock waits, connection count, cache hit ratio, and storage growth before you tune anything else. The queue is rarely the first thing to break; the database under it is.