Skip to content

Connections are the scarce resource, not memory

PostgreSQL runs one operating system process per connection. On a small server that decides how many tenants you can have, long before disk or RAM does.

Kerem Sinecek2 min read
View as Markdown

The number everyone reaches for first

Ask how large a managed Postgres can be and the answer usually starts with memory. On our machine it does not. PostgreSQL forks a process for every connection, and each of those costs a few megabytes of private memory before it executes a single statement. The server-wide connection limit is therefore not a tuning knob, it is a capacity plan.

What the platform is actually configured for

The cluster allows two hundred connections, three of them reserved for the superuser. That leaves one hundred and ninety-seven usable slots, and almost all of them are already handed out as fixed per-role reservations rather than consumed.

textReservations, not usage. Actual concurrent connections sit in the single digits.
max_connections                200
superuser_reserved_connections   3
usable                         197
sum of role limits             187

Why the allocation runs out before the server does

Every customer database role carries its own connection limit, and that limit is reserved whether or not anybody uses it. The result is a platform that is full at the allocation layer while the machine is nearly idle. That is the honest reason the first cohort is small: not disk, not memory, not CPU, but how many roles fit inside one hundred and ninety-seven.

Raising the limit does not create capacity

Doubling the server-wide limit buys parked sessions, not throughput. With two cores the number of queries that can genuinely run at once is a handful; everything above that is context switching. The number also pushes up the worst-case memory total, which is the figure the kernel's out-of-memory killer cares about.

  • One operating system process per connection, no built-in pooling
  • Roughly five to ten megabytes of private memory per idle backend
  • A work memory allowance on top of that for every sort or hash in a query
  • Useful parallelism is bounded by cores, not by the connection limit

What actually adds tenants

A connection pooler in transaction mode in front of the customer roles. Ten tenants with ten client connections each can then share a far smaller number of real server connections, because a client only occupies one for the duration of a transaction rather than for the duration of its session. That is the change that buys room without buying hardware, and it is the one we will make before the allocation is exhausted rather than after.

Why we publish the number

Because a capacity limit that customers discover by hitting it is worse than one they read in advance. The figure is checked whenever a role limit changes, it is exposed to the operator, and it is the reason access is granted in waves instead of on demand.