Configure

Pooling

How StreamQL shares server connections between clients, and what it does when a client needs more.

Pooling modes#

ModeA server connection is heldUse it for
transaction (default)for one transactionMost applications. The most sharing.
sessionfor the whole client sessionClients that depend on session state or session-level features.
statementfor one statementAutocommit workloads. A BEGIN is rejected with error 0A000.

Set the default with defaults.mode or --pool-mode, and override it per pool with [[pool]] mode. Changing a mode takes effect after a drain.

Session state and pinning#

In transaction mode a client may land on a different server connection for each transaction. StreamQL tracks session state so the client does not notice:

  • SET, RESET and RESET ALL, startup parameters and the options parameter are tracked and replayed as a minimal diff on the connection the client gets next.
  • When a server connection goes back to the pool it is reset with ROLLBACK and DISCARD ALL.
  • Some statements tie a client to one server connection: LISTEN, advisory locks, temporary objects, WITH HOLD cursors, SQL PREPARE, and calls to functions outside the built-in allowlist. The session is pinned and the reason is recorded, for example POOL_PINNED_ADVISORY_LOCK.
  • session.pin_unknown_functions controls the last case, and session.attested_functions lists functions you declare free of session effects.
  • defaults.max_pinned_fraction (0.9) caps the share of a pool that may be pinned.

Prepared statements#

Prepared statements work across pooled backends in transaction mode. Each client has its own statement namespace, and each backend keeps the statements it holds, up to defaults.statements_per_backend (500) with least-recently-used eviction. A Parse between transactions does not wait for a server connection: the statement is prepared lazily at Bind or Describe, so under contention a parse error surfaces there.

Queueing and deadlines#

When every server connection of a pool is busy, clients wait in a queue. After queue_timeout_ms (5000 by default) the client receives error 53000 and its session is kept. The wait is recorded as pool wait, which the console shows next to database time.

Reserve pools#

reserve = { size, after_wait_ms, max_slowdown } lets a pool open extra server connections, but only after a client has waited after_wait_ms. The reserve is withheld while recent latency is above max_slowdown times its baseline or while a connection attempt recently failed, and it is closed again on release when nobody is waiting.

Per-tenant share#

tenant = { key, share } caps the fraction of max_size one tenant may hold. The tenant comes from a tracked setting named by key, such as app.tenant_id. Excess leases queue and then fail with 53000.

Connection budget and limits#

  • defaults.max_size bounds server connections per database and role.
  • max_db_connections bounds server connections to one database across all users. Idle backends of sibling pools are evicted to make room.
  • limits.max_user_connections bounds one backend role across all databases, and limits.max_user_clients / max_db_clients bound clients; both fail with 53300 at login.
  • [budget] declares how many connections this member may hold against PostgreSQL max_connections. In a fleet the primary hands each worker a share of it.

Data-plane engine#

The default engine is tokio. The per-core reactor engine is opt-in with dataplane.engine = "reactor" or STREAMQL_DATAPLANE__ENGINE=reactor. It needs plain-TCP upstreams: a client that negotiated TLS, or a pool whose servers use TLS, stays on the tokio engine with a reason code. All pooling features run on both engines, and setting the engine back to tokio reverts completely.

environment
STREAMQL_DATAPLANE__ENGINE=reactor
STREAMQL_UPSTREAM__SSLMODE=disable

Pool-only mode#

Everything on this page also holds when you run StreamQL without the console. Set admin.enabled = false (or pass --no-admin) and insights.enabled = false, or use the pool-only build. The pooling modes, session-state safety, queues and limits do not change; the console, admin API and /metrics are not available. See Choose an edition.

Graceful drain#

On SIGTERM, or POST /api/v1/drain, a node turns readiness off, keeps accepting for drain.endpoint_propagation_ms so load balancers notice, stops accepting, gives open transactions drain.grace_s to finish, and closes remaining clients with error 57P01.