Questo articolo fornisce la formula pubblicata dal wiki di PostgreSQL per calcolare la concorrenza ideale del tuo hardware (la formula di dimensionamento del pool di connessioni più citata nell'ecosistema), spiega perché ogni connessione costa più di un thread dell'applicazione, quindi descrive in dettaglio la procedura per impostare max_connections senza indovinare. La nostra metodologia di benchmark documenta il protocollo di misurazione utilizzato per qualsiasi affermazione sulle prestazioni su questo blog.
L'essenziale
- Senza pooling, max_connections dovrebbe coprire tutte le connessioni client simultanee, non solo quelle che Postgres può elaborare in modo efficiente in parallelo.
- Formula di riferimento wiki PostgreSQL: concorrenza attiva ideale = (core fisici × 2) + dischi efficienti. Un punto di partenza da convalidare mediante misurazione, non un limite rigido.
- max_connections è un parametro di contesto
postmaster: modificarlo richiede un riavvio completo del server, non un semplice ricaricamento. - Ogni connessione Postgres è un processo di sistema separato, non un thread leggero: questo è ciò che rende reale il sovraccarico non appena il numero di connessioni aumenta.
- Verificato nel codice: sui suoi cluster Postgres dedicati, Aurabase varia max_connections da 50 (livello gratuito) a 400 (livello enterprise) a seconda della dimensione del cluster.
Perché una connessione Postgres costa più di un thread dell'applicazione
Postgres non utilizza un pool di thread leggero per le sue connessioni. Ogni connessione client attiva un processo di sistema completo.
Il processo postmaster ne crea uno nuovo (“fork”) per ogni tentativo di connessione, dedicato a questa singola sessione fino alla sua chiusura. La documentazione ufficiale del progetto descrive precisamente questo meccanismo nel capitolo sui fondamenti dell'architettura (postgresql.org/docs/current/connect-estab.html, sezione "Connection Semantics", accesso il 24 agosto 2026).
Questa scelta ha un vantaggio reale: un crash su una connessione non influisce sulle altre, essendo ogni processo isolato dal resto del server. Ha anche un costo diretto: ogni connessione aggiuntiva aggiunge alla pianificazione un intero processo del sistema operativo, con il proprio spazio di memoria e il proprio sovraccarico di commutazione del contesto per il kernel.
Un'applicazione che apre 500 connessioni dirette a Postgres senza pooling costringe il server a gestire 500 processi di sistema simultanei, anche se la stragrande maggioranza di essi rimane inattiva tra due richieste.
Cosa consuma effettivamente una connessione: memoria condivisa e work_mem
Due meccanismi distinti incidono sulla memoria e confonderli porta quasi sempre a una diagnosi errata.
Il primo è fisso. All'avvio Postgres riserva strutture di memoria condivisa (lock, tabella dei processi) dimensionate sul valore di max_connections, indipendentemente dal fatto che tali connessioni vengano successivamente aperte o meno. La documentazione ufficiale per l'impostazione sottolinea esplicitamente: aumentarla potrebbe richiedere più memoria condivisa del sistema rispetto a quella consentita dalla configurazione predefinita del sistema operativo (postgresql.org/docs/current/runtime-config-connection.html, accesso il 24 agosto 2026).
Il secondo è variabile e molto più pericoloso su larga scala: work_mem non viene allocato una volta per connessione, ma una volta per operazione di ordinamento o hashing nel piano di query. La documentazione ufficiale è esplicita su questo punto: una query complessa può avviare diverse di queste operazioni in parallelo, e più sessioni possono fare la stessa cosa contemporaneamente, in modo che la memoria effettivamente utilizzata possa valere più volte work_mem (postgresql.org/docs/current/runtime-config-resource.html, ultimo accesso 24 agosto 2026).
Non è solo max_connections × work_mem a minacciare la memoria di un server. È max_connections × work_mem × numero di operazioni simultanee per query. È questo prodotto che spiega un server che effettua uno scambio o che esaurisce la memoria dopo un aumento di max_connections considerato innocuo.
La formula di dimensionamento del wiki PostgreSQL
Il wiki ufficiale del progetto PostgreSQL documenta una formula di riferimento per calcolare quante connessioni attive il tuo hardware può elaborare in modo efficiente in parallelo, non quante connessioni aperte in totale (wiki.postgresql.org/wiki/Number_Of_Database_Connections, accesso il 24 agosto 2026).
concorrenza attiva ideale = (core fisici × 2) + dischi efficienti. Il numero di core esclude l'hyperthreading. Il numero di dischi effettivi rimane vicino a 1 sui moderni dispositivi di archiviazione SSD, dove la nozione di disco fisico separato (“mandrino”) perde gran parte del suo significato originale.
Su un server con 8 core fisici e storage SSD, la formula fornisce (8 × 2) + 1 = 17 connessioni attive prima che la velocità effettiva inizi a diminuire. Questo dato spesso sorprende: sembra minuscolo rispetto alle centinaia di connessioni che un'applicazione apre nella pratica. Questo è proprio l'oggetto del paragrafo successivo.
Il numero calcolato dalla formula misura la concorrenza che la CPU e il disco possono assorbire, non il numero di connessioni client che l'applicazione deve aprire. Una flotta di 20 processi applicativi, ciascuno con il proprio pool di 10 connessioni, apre 200 connessioni simultanee a Postgres anche se solo 17 di esse lavorano attivamente in un dato momento. Senza un pooler, max_connections deve coprire 200, non 17. È questo divario che spinge la maggior parte delle architetture ad aggiungere un pooler in modalità transazione, anche se ciò significa scegliere quale (vedi il nostro confronto PgBouncer, Supavisor e PgCat).
Come modificare max_connections (e perché è necessario un riavvio)
max_connections non viene sostituito a caldo. Questo è un parametro di contesto postmaster: Postgres lo legge una volta, all'avvio, per dimensionare la sua memoria condivisa. Un ricaricamento della configurazione (pg_reload_conf() o SIGHUP) non è sufficiente, è necessario riavviare il server.
Per prima cosa controlla il valore corrente e il suo contesto, per confermare che sarà necessario un riavvio:
Quindi applica il nuovo valore, quindi riavvia:
max_connections include di default superuser_reserved_connections (3 di default): queste connessioni sono riservate ad un superutente in caso di saturazione, non sono mai disponibili per la tua applicazione, anche se il contatore globale non è ancora stato raggiunto.
Come Aurabase pianifica il budget di max_connections sui suoi cluster Postgres
Il dimensionamento di max_connections non è solo un esercizio teorico. Ecco come Aurabase lo budgeta sui suoi cluster Postgres gestiti:
Cluster dedicati: un cluster Postgres per progetto
A questo livello (vedi il nostro confronto dedicato vs base condivisa), ogni progetto riceve il proprio cluster CloudNativePG e il proprio budget max_connections, dimensionato con la dimensione dell'istanza:
| gratuito (dedicato) | connessioni_massime 50 | 1 istanza · 500 milioni di vCPU · 512 Mi |
|---|---|---|
| professionista (predefinito) | connessioni_massime 200 | 2 istanze · 1 vCPU · 2Gi |
| squadra | connessioni_massime 300 | 3 istanze · 2 vCPU · 3Gi |
| affari | max_connessioni 400 | 3 istanze · 2 vCPU · 4Gi |
Cluster condivisi: diversi progetti di un'organizzazione, un budget condiviso
In questo secondo percorso, tutti i progetti della stessa organizzazione si collegano tramite un pooler CNPG (PgBouncer, modalità transaction) davanti ad un primario condiviso:
| gratuito | connessioni_massime 50 | max_client_conn 100 | max_user_connections 20 |
|---|---|---|---|
| pro | connessioni_massime 100 | max_client_conn 200 | max_user_connections 60 |
| squadra | connessioni_massime 200 | max_client_conn 400 | max_user_connections 150 |
Tutti i progetti di un'organizzazione si connettono tramite un ruolo applicativo condiviso. max_user_connections quindi da solo limita le connessioni server totali che questo ruolo può aprire nell'intero cluster: questa è la vera salvaguardia globale del cluster, non max_client_conn, che limita solo le connessioni client al pooler stesso.
Tuttavia, questo pooler serve solo il traffico dell'applicazione SDK. PostgREST, dal canto suo, rimane direttamente connesso al primario (servizio-rw): il pooling in modalità transazione interromperebbe il suo meccanismo di ricaricamento dello schema, che ascolta un canale dedicato LISTEN denominato pgrst. Le sue stesse connessioni (2 per replica a livello condiviso, 10 per replica a livello dedicato) contano quindi direttamente nel budget max_connections del primario, al di fuori di qualsiasi pooler, esattamente il tipo di connessione “dimenticata” che il passo 1 della procedura seguente deve includere.
Il codice documenta esplicitamente questi budget del pooler condiviso come valori iniziali da calibrare in condizioni reali, misurando pg_stat_activity sotto carico, non come cifre fisse da un benchmark pubblicato. Questa è la stessa disciplina descritta nella nostra metodologia di benchmark : misurare prima di aggiustare, non indovinare e poi sperare. Questi cluster vengono eseguiti su PostgreSQL 16, una scelta documentata nel nostro confronto Postgres 16 vs 17 vs 18.
La procedura in 5 passaggi per dimensionare max_connections senza pooling
Questa procedura non dipende da nessuno strumento particolare: si applica a qualsiasi server Postgres, gestito o self-hosted.
- Conta le tue connessioni client effettive. Numero di processi applicativi moltiplicato per la dimensione del pool interno, più strumenti di amministrazione, replica e monitoraggio. È questo numero, non la formula, che imposta il limite massimo di max_connections.
- Calcola la concorrenza ideale del tuo hardware con la formula del wiki PostgreSQL: (core fisici × 2) + dischi efficienti. Questa cifra indica quante di queste connessioni possono effettivamente funzionare in parallelo senza ridurre la velocità effettiva.
- Imposta max_connections al di sopra della necessità effettiva per il passaggio 1, con margine per
superuser_reserved_connectionse per eventuali strumenti di amministrazione che aprono le proprie connessioni all'esterno dell'applicazione. - Applicare la modifica con ALTER SYSTEM SET, quindi riavviare il server. Questo è un parametro postmaster: non è sufficiente un semplice ricaricamento, come spiegato sopra.
- Monitora pg_stat_activity nel tempo. Se il numero di connessioni inattive supera di molto il numero di connessioni attive, questo non è un problema di max_connections: è il segnale che è necessario un pooler davanti al server, non un numero più alto.
La richiesta di monitoraggio dal passaggio 5, direttamente utilizzabile:
Quando la formula non basta più: i segnali che hai bisogno di un pooler
Tre segnali ritornano sistematicamente quando max_connections da solo non è più sufficiente, qualunque sia il suo valore.
- L'errore
FATAL: sorry, too many clients alreadyviene visualizzato durante i picchi di carico, mentre la maggior parte delle connessioni visualizzate dapg_stat_activitysono in stato inattivo. - L'applicazione viene eseguita in un ambiente serverless o con lavoratori temporanei (funzioni edge, lavori brevi), che aprono e chiudono connessioni molto più velocemente di quanto il modello processo per connessione di Postgres sia stato progettato per soddisfare.
- La formula e la procedura di cui sopra sono già state applicate e l'effettiva necessità di connessioni client continua a superare ciò che la memoria disponibile può allocare senza mettere in pericolo work_mem o 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.