max_connections is a memory budget and a circuit breaker wearing a capacity costume. It does not decide how many queries your server can run at once; the core count and the storage decide that, and they were decided when you bought the hardware. What max_connections decides is how many client backends PostgreSQL will bring into existence before it starts saying no, and, less obviously, how much shared memory it sets aside on the assumption that every one of them might show up.
The default is 100. The context is postmaster: you cannot change it without a restart, which is the sentence to remember at 2 a.m. The range is 1 to 262,143 (2^18 − 1, because the buffer header has eighteen bits for its pin count and every backend needs one). Not all of the slots are yours: superuser_reserved_connections holds three of them back for superusers, and since PostgreSQL 16 reserved_connections (default zero) can hold back more for members of pg_use_reserved_connections. A default install therefore gives ordinary roles 97 connections. Ask for a 98th on 18 and you get FATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute (older versions word it differently); fill the superuser slots too and the message becomes the older and better-known FATAL: sorry, too many clients already.
Here is what actually scales with the number. Each connection is an operating system process with its own PGPROC entry in shared memory, its own row in pg_stat_activity, its own share of the lock manager’s hash tables (sized as max_locks_per_transaction times max_connections plus max_prepared_transactions), its own predicate-lock bookkeeping, and, in 18, its own set of asynchronous I/O handles. PostgreSQL allocates all of it at startup whether or not anyone ever connects. On an 18.6 instance with the default shared_buffers, total shared memory is 150 MB at max_connections = 100, 197 MB at 1,000, and 389 MB at 5,000, and the two lock managers’ tables are most of the growth. The documentation says only that “PostgreSQL sizes certain resources based directly on the value of max_connections”; now you know which ones.
Because those structures exist on the standby too, and the standby has to replay whatever the primary did with them, a hot standby refuses to run with a smaller max_connections than its primary. At startup it simply won’t; mid-stream, when the parameter change arrives in the WAL, it logs a warning, pauses recovery, and waits for you to fix the setting and restart. The order of operations follows: raise the value on the standbys first, then the primary; lower it on the primary first, then the standbys. Get it backwards and the standby will explain the rule to you in the log, at length, while your replication lag grows.
What a connection costs
Less than it used to, and more than the cloud providers think.
An idle backend is cheap. A freshly connected one on my 18.6 test instance had about a megabyte of private memory, and it grows from there as the relation and catalog caches fill in. The reason PostgreSQL historically “didn’t handle thousands of connections” was not memory but snapshots: every statement takes one, and taking one meant walking the array of every backend’s transaction state, idle backends included. With a few thousand connections that walk was a measurable fraction of every query. Andres Freund’s analysis of the problem and his fix for it landed in PostgreSQL 14, and idle connections have been far less expensive since. If your objection to a high max_connections is “idle connections are expensive,” update the objection.
Active connections are the problem, and they always were. A machine with sixteen cores executes sixteen things at once. The seventeenth active backend is not additional throughput; it is a context switch, a spinlock, and a longer queue on whichever lightweight lock the other sixteen are contending for. Past a small multiple of the core count, adding active backends reduces throughput, and the reduction is not gentle. The memory story is the same story: work_mem is a per-operation allowance, not a per-connection one, and 400 backends each running a query with three hash joins can ask for 1,200 allocations of it at the same instant. That is the arithmetic that turns “we have plenty of RAM” into the OOM killer taking out the postmaster, and it is precisely the arithmetic max_connections exists to bound.
The signs are not subtle. pg_stat_activity shows hundreds of rows with state = 'active' on a box with a dozen cores; the same view fills with wait_event_type = 'LWLock' and wait_event = 'LockManager'; load average sails past the core count; and pg_stat_database.numbackends is a number nobody on the team chose.
How we got to five thousand
For most of PostgreSQL’s life the answer to “how many connections?” was 100 unless you had a reason, and the managed services agreed. Heroku Postgres capped its largest plans at 500 in 2013 and wrote a blog post explaining that nobody who ran Postgres at scale encouraged going anywhere near that. Then RDS. Its PostgreSQL parameter groups of the mid-2010s set max_connections to {DBInstanceClassMemory/31457280}: 30 MiB of instance memory per connection slot, so a 1 GiB db.t1.micro got 26. Somewhere between 2016 and 2018 the formula became LEAST({DBInstanceClassMemory/9531392},5000). That is 9.1 MiB per slot, roughly three times as many slots, with a ceiling of five thousand. Aurora PostgreSQL uses the same formula. A 16 GiB instance gets about 1,700 connections; anything from 48 GiB up gets 5,000.
Microsoft then adopted it. Azure Database for PostgreSQL documents its default as MIN(memoryGib * 0.1049164697034809, 5000), and 0.1049164697034809 is 1,000,000 ÷ 9,531,392 to the last digit: the RDS divisor with the units changed and the same cap. (The docs say gibibytes; the arithmetic only works in megabytes; a 16 GiB server gets 1,718.) Google, to its credit, did not follow. Cloud SQL scales the default with memory too, but a 16 GB instance gets 500 and nothing gets more than 1,000. And Heroku, which held the line at 500 for a decade, now offers 5,000 per instance on its new Advanced tier. The number has become a feature.
So the default for a mid-sized cloud PostgreSQL is now somewhere between seventeen and fifty times the community default, produced by a formula whose only input is RAM and whose implicit model is that every byte of it can go to backend processes. Nothing in it knows about cores, or shared_buffers, or work_mem. And AWS’s own knowledge center advises, of this same number: “Don’t increase the max_connections parameter beyond the default value.” What it recommends instead is RDS Proxy. RDS Proxy is a connection pooler, which is correct, and it is billed separately, which is convenient.
Here is what a five-thousand-connection default actually does. It removes the one error message that reliably gets a connection pooler installed. sorry, too many clients already is loud, it is early, and it arrives at the application tier, where the people who can fix it are looking. A team whose app opens a connection per worker thread across forty autoscaled pods hits that wall at 100, reads the message, installs a pooler, and moves on with their lives. The same team on RDS never hits the wall. They hit the traffic spike instead, with 1,400 active backends on eight vCPUs, and what they see is not an error but a database that has stopped answering, a FreeableMemory graph heading for zero, and a failover that does not help because the replica inherited the same parameter group. The high ceiling did not make the failure go away. It moved the failure from the client, where it was cheap and legible, to the server, where it is an outage. Every connection-storm incident PGX has been called into in recent years was on a server with max_connections in the thousands, and not one of them had run out of slots.
The setting should be small, and the way to make it small is to stop letting the application decide. Put PgBouncer (or whatever pooler your platform ships) in transaction-pooling mode in front of the database, and let it absorb the application’s opinion of how many connections it needs. Then set max_connections to what the poolers will actually open, plus the replication connections, plus a couple of dozen for humans and monitoring. For most production systems that lands between 100 and 300. Cap any single application with ALTER ROLE ... CONNECTION LIMIT so one runaway deploy cannot eat the whole budget, let idle_in_transaction_session_timeout reclaim the slots that leak, and check the arithmetic against a couple of weeks of pg_stat_database.numbackends. If you inherited 1,700 from a parameter group, lower it; that is a primary-first change and a restart (on RDS, a custom parameter group and a reboot), and it is worth the window.
A max_connections in the thousands is not a configuration. It is an abdication, and the bill for it arrives during the one traffic spike you actually cared about.