PRODSovereign European BaaS platformOpen Dashboard →

Performance · 9 min read

Postgres max_connections without a pooler

Affane Daylami · Fondateur · June 6, 2026

Back to blog

Without pooling in front of your server, max_connections should cover every open client connection at the same time, not the number of requests Postgres can efficiently process in parallel. Confusing these two numbers is the most common cause of incorrectly set max_connections: too low to absorb the load, or too high for the actually available memory.

This English text was generated automatically from the French original and has not been reviewed yet.

This article gives the formula published by the PostgreSQL wiki to calculate the ideal concurrency of your hardware (the most cited connection pool sizing formula in the ecosystem), explains why each connection costs more than an application thread, then details the procedure for setting max_connections without guessing. Our benchmark methodology documents the measurement protocol used for any performance claims on this blog.

The essentials

  • Without pooling, max_connections should cover all concurrent client connections, not just those that Postgres can efficiently process in parallel.
  • PostgreSQL wiki reference formula: ideal active concurrency = (physical cores × 2) + efficient disks. A starting point to be validated by measurement, not a hard limit.
  • max_connections is a postmaster context parameter: changing it requires a complete restart of the server, not a simple reload.
  • Each Postgres connection is a separate system process, not a lightweight thread: this is what makes the overhead real as soon as the number of connections climbs.
  • Verified in the code: on its dedicated Postgres clusters, Aurabase varies max_connections from 50 (free tier) to 400 (enterprise tier) depending on the size of the cluster.
#
Diagnosis

Why a Postgres connection costs more than an application thread

Postgres does not use a lightweight thread pool for its connections. Each client connection triggers a full-fledged system process.

The postmaster process creates a new one (“fork”) for each connection attempt, dedicated to this single session until it is closed. The official project documentation describes precisely this mechanism in its chapter on architectural fundamentals (postgresql.org/docs/current/connect-estab.html, “Connection Semantics” section, accessed August 24, 2026).

This choice has a real advantage: a crash on one connection does not affect the others, each process being isolated from the rest of the server. It also has a direct cost: each additional connection adds an entire OS process to schedule, with its own memory space and its own context switching overhead for the kernel.

What it changes in practice

An application that opens 500 direct connections to Postgres without pooling forces the server to manage 500 simultaneous system processes, even if the vast majority of them remain idle between two requests.

#
Memory cost

What a connection actually consumes: shared memory and work_mem

Two distinct mechanisms impact memory, and confusing them almost always leads to a misdiagnosis.

The first is fixed. At startup, Postgres reserves shared memory structures (locks, process table) sized on the value of max_connections, whether or not these connections are subsequently opened. The official documentation for the setting explicitly points out: increasing it may require more system shared memory than your OS's default configuration allows (postgresql.org/docs/current/runtime-config-connection.html, accessed August 24, 2026).

The second is variable, and much more dangerous at scale: work_mem is not allocated once per connection, but once per sorting or hashing operation in the query plan. The official documentation is explicit on this point: a complex query can launch several of these operations in parallel, and several sessions can do the same simultaneously, so that the memory actually used can be worth several times work_mem (postgresql.org/docs/current/runtime-config-resource.html, accessed August 24, 2026).

The real worst case to remember

It's not max_connections × work_mem alone that threatens a server's memory. It is max_connections × work_mem × number of concurrent operations per query. It is this product which explains a server which swaps, or which runs out of memory after an increase in max_connections considered innocuous.

#
Formula

The PostgreSQL wiki sizing formula

The official PostgreSQL project wiki documents a benchmark formula for calculating how many active connections your hardware can efficiently process in parallel, not how many connections open in total (wiki.postgresql.org/wiki/Number_Of_Database_Connections, accessed August 24, 2026).

The formula

ideal active concurrency = (physical cores × 2) + efficient disks. The number of cores excludes hyperthreading. The number of effective disks remains close to 1 on modern SSD storage, where the notion of a separate physical disk (“spindle”) loses much of its original meaning.

On a server with 8 physical cores and SSD storage, the formula gives (8 × 2) + 1 = 17 active connections before throughput begins to degrade. This figure is often surprising: it seems tiny compared to the hundreds of connections that an application opens in practice. This is precisely the subject of the following paragraph.

The number calculated by the formula measures the concurrency that the CPU and disk can absorb, not the number of client connections your application needs to open. A fleet of 20 application processes, each with its own pool of 10 connections, opens 200 simultaneous connections to Postgres even if only 17 of them are actively working at any given time. Without a pooler, max_connections must cover the 200, not the 17. It is this gap that pushes most architectures to add a pooler in transaction mode, even if it means choosing which one (see our comparison PgBouncer, Supavisor and PgCat).

#
Procedure

How to change max_connections (and why a reboot is required)

max_connections is not hot swapped. This is a postmaster context parameter: Postgres reads it once, at startup, to size its shared memory. A configuration reload (pg_reload_conf() or SIGHUP) is not enough, you must restart the server.

First check the current value and its context, to confirm that a restart will be necessary:

psqlsql
SHOW max_connections;

SELECT name, setting, context
FROM pg_settings
WHERE name = 'max_connections';

-- context='postmaster' confirms that a reboot is required

Then apply the new value, then restart:

psqlsql
ALTER SYSTEM SET max_connections = '300';

-- Written in postgresql.auto.conf.
-- No effect until Postgres is restarted.
terminalbash
# With systemd
sudo systemctl restart postgresql

# Without systemd, directly with pg_ctl
pg_ctl restart -D $PGDATA -m fast
A margin that many forget

max_connections includes by default superuser_reserved_connections (3 by default): these connections are reserved for a superuser in case of saturation, they are never available for your application, even if the global counter is not yet reached.

#
Checked in code

How Aurabase budgets max_connections on its Postgres clusters

Sizing max_connections is not just a theoretical exercise. Here is how Aurabase budgets it on its managed Postgres clusters:

100
POSTGRES DEFAULT
max_connections before any tuning
50→400
DEDICATED AURABASE BEARINGS
free to enterprise, by CNPG cluster
3
SUPERUSER RESERVED
superuser_reserved_connections, Postgres default

Dedicated clusters: one Postgres cluster per project

On this level (see our comparison dedicated vs shared base), each project receives its own CloudNativePG cluster and its own max_connections budget, sized with the size of the instance:

free (dedicated)max_connections 501 instance · 500m vCPU · 512Mi
pro (default)max_connections 2002 instances · 1 vCPU · 2Gi
teammax_connections 3003 instances · 2 vCPU · 3Gi
businessmax_connections 4003 instances · 2 vCPU · 4Gi

Shared clusters: several projects of an organization, a shared budget

On this second path, all the projects of the same organization connect via a CNPG pooler (PgBouncer, transactionmode) in front of a shared primary:

freemax_connections 50max_client_conn 100max_user_connections 20
promax_connections 100max_client_conn 200max_user_connections 60
teammax_connections 200max_client_conn 400max_user_connections 150

All projects in an organization connect through a shared application role. max_user_connections therefore alone caps the total server connections that this role can open across the entire cluster: this is the real cluster-global safeguard, not max_client_conn, which only limits client connections to the pooler itself.

However, this pooler only serves SDK application traffic. PostgREST, for its part, remains directly connected to the primary (-rwservice): pooling in transaction mode would break its schema reloading mechanism, which listens to a dedicated LISTEN channel named pgrst. Its own connections (2 per replica on the shared level, 10 per replica on the dedicated level) therefore count directly in the max_connections budget of the primary, outside of any pooler, exactly the kind of “forgotten” connection that step 1 of the procedure below must include.

Figures currently being calibrated, assumed as such

The code explicitly documents these shared pooler budgets as starting values to be calibrated in real conditions, by measuring pg_stat_activity under load, not as fixed figures from a published benchmark. This is the same discipline as described in our benchmark methodology: measure before adjusting, not guess then hope. These clusters run on PostgreSQL 16, a choice documented in our Postgres 16 vs 17 vs 18comparison.

#
Method

The 5-step procedure to size max_connections without pooling

This procedure does not depend on any particular tool: it applies to any Postgres server, managed or self-hosted.

  1. Count your actual client connections. Number of application processes multiplied by the size of their internal pool, plus administration tools, replication and monitoring. It is this number, not the formula, that sets the max_connections floor.
  2. Calculate the ideal concurrency of your hardware with the formula from the PostgreSQL wiki: (physical cores × 2) + efficient disks. This figure indicates how many of these connections can actually work in parallel without degrading throughput.
  3. Set max_connections above the actual need for step 1, with margin for superuser_reserved_connections and for any admin tools that open their own connections outside the application.
  4. Apply the change with ALTER SYSTEM SET, then restart the server. This is a postmaster parameter: a simple reload is not enough, as detailed above.
  5. Monitor pg_stat_activity over time. If the number of idle connections greatly exceeds the number of active connections, this is not a max_connections problem: it is the signal that you need a pooler in front of the server, not a higher number.

The monitoring request from step 5, directly usable:

psqlsql
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state
ORDER BY count(*) DESC;
#
Warning signal

When the formula is no longer enough: the signs that you need a pooler

Three signals systematically return when max_connections alone is no longer sufficient, whatever its value.

  1. The FATAL: sorry, too many clients already error appears during peak load, while the majority of connections displayed by pg_stat_activity are in the idle state.
  2. The application runs in a serverless environment or with ephemeral workers (edge functions, short jobs), which open and close connections much faster than Postgres' process-per-connection model was designed to accommodate.
  3. The above formula and procedure have already been applied, and the actual need for client connections continues to exceed what available memory can allocate without endangering work_mem or shared_buffers.

In these three cases, the correct answer is almost always a pooler positioned between the application and Postgres, not a higher max_connections. Our comparison PgBouncer, Supavisor and PgCat details the three options, and our guide to transaction mode explains the most common compromise once the pooler is in place. For all Postgres tuning beyond connections, see our production Postgres tuning checklist.

#
Frequently Asked Questions

FAQs

What is PostgreSQL's default max_connections?+
100, with 3 connections reserved for the superuser by default (superuser_reserved_connections). This default is suitable for many applications that pass through a pooler, but quickly becomes insufficient without pooling as soon as a fleet of application processes each opens its own batch of connections.
Can we change max_connections without restarting PostgreSQL?+
No. max_connections is a postmaster context parameter: Postgres reads it once at startup to size its shared memory. ALTER SYSTEM SET writes the new value to postgresql.auto.conf, but only a full server restart applies it; a reload or a SIGHUP are not enough.
How much memory does an idle PostgreSQL connection consume?+
There is no single official number: it depends on work_mem, shared_buffers and extensions loaded per session. What is documented, however, is that work_mem is allocated per sorting or hashing operation in a query, not per connection: a single complex query can therefore consume work_mem several times on a single active connection.
Should we always prefer a pooler like PgBouncer to a higher max_connections?+
In the majority of cases, yes, as soon as the number of actual client connections greatly exceeds the ideal concurrency calculated by the PostgreSQL wiki formula. A transaction mode pooler pools a small number of physical connections between a much larger number of logical connections on the application side. See our comparison of PgBouncer, Supavisor and PgCat to choose which one.
What exactly does the formula (cores × 2) + efficient disks measure?+
It estimates the ideal active concurrency: the number of requests that a given server's CPU and disk can process in parallel without degrading throughput, not the total number of connections to open in max_connections. This is a starting point to be validated by measurement, documented by the official PostgreSQL project wiki, not a hard limit.
How do I know if my Postgres server is close to its connections limit?+
Query pg_stat_activity and compare the number of connections in active state to those in idle state. A large number of idle connections near the max_connections cap, with no active request behind it, almost always indicates a need to pool rather than a need to raise max_connections further.

READY TO DEPLOY?

Your backend in five minutes.

No credit card required · 500 MB free · 50,000 MAU