Supabase 'max clients reached in session mode' su Vercel, e la correzione che ha peggiorato le cose
L’errore è (EMAXCONNSESSION) max clients reached in session mode - max clients are limited to pool_size: 15. La nostra API ha iniziato a restituirlo mentre il sito marketing restava perfettamente sano, perché il sito marketing non tocca Postgres. È proprio per questo che questo tipo di guasto passa inosservato. Le parti che sembrano a posto sono quelle che non usano il database.
Ecco cosa significa l’errore, i due errori che abbiamo fatto correggendolo, e la stringa di connessione che funziona davvero da Vercel.
Due pooler, due porte
Perché il serverless esaurisce la session mode
Ogni istanza di funzione calda tiene un suo piccolo pool. Nel nostro caso ne teneva tre. In session mode un pool non restituisce mai le connessioni finché l’istanza è calda. Cinque istanze calde fanno quindici connessioni, cioè il limite. La sesta richiesta, da qualsiasi parte arrivi, fallisce.
Un daemon della flotta che interrogava tre endpoint ogni pochi secondi teneva le istanze calde giorno e notte. Non c’è niente di insolito. È solo un conto che la session mode perde.
Errore uno: correggere prima il livello sbagliato
La correzione d’emergenza è stata max: 1 per istanza e un idle timeout di venti secondi, sempre in session mode. Ha funzionato. Le istanze inattive restituivano il loro posto, e l’API si è ripresa in pochi minuti. Poi abbiamo spostato la stringa di connessione sulla transaction mode, ed era la cosa giusta. Abbiamo lasciato max: 1, e quella non lo era.
Errore due: un pool da uno in transaction mode
In transaction mode, una sola connessione per istanza mette in fila ogni query di quell’istanza dietro un unico canale. Il polling del daemon si è accodato lì dietro. Le richieste hanno superato la scadenza di venti secondi, e il sintomo è passato dagli errori 500 ai blocchi. Il sito marketing era ancora veloce. L’API ha smesso del tutto di rispondere.
La transaction mode restituisce la connessione dopo ogni transazione, quindi vuole un pool di dimensione normale. Abbiamo impostato otto e i timeout sono spariti.
Come vederlo arrivare
Il pooler non ti dice nulla finché non ti rifiuta. Postgres invece te lo dice, se glielo chiedi.
select application_name, state, count(*) from pg_stat_activity where datname = 'postgres' group by 1, 2 order by 3 desc;
Esegui questa query su una connessione in session mode mentre l’app è sotto carico normale. Se il conteggio si avvicina al limite e la maggior parte delle righe è idle, sei a un minuto intenso dall’errore. In transaction mode la stessa query mostra connessioni riciclate invece che accumulate.
La stringa di connessione che funziona davvero
postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres
- L’host è il pooler, non
db.<ref>.supabase.co. La dashboard di Supabase può mostrare l’host diretto sotto un titolo da transaction pooler. Resta comunque l’host diretto. - Lo username è
postgres.<project-ref>sul pooler. Il semplicepostgresfunziona solo sull’host diretto. - L’host diretto è solo IPv6, a meno che tu non compri l’add-on IPv4. Ha un record AAAA e nessun record A. Le funzioni Vercel non lo raggiungono, e comunque lì la porta 6543 non è aperta.
- Nessuna query string necessaria con postgres.js. Imposta
prepare: false. Il flag?pgbouncer=trueè una convenzione di Prisma. - Tieni le migrazioni sulla 5432. Drizzle e strumenti simili vogliono una connessione in session mode. Punta l’app in produzione sulla 6543 e l’ambiente locale delle migrazioni sulla 5432.
$ dig +short A db.<project-ref>.supabase.co # (nothing) $ dig +short AAAA db.<project-ref>.supabase.co # 2600:1f18:... $ nc -z aws-0-us-east-1.pooler.supabase.com 6543 # succeeded
Il dimensionamento, in una tabella
Un’ultima cosa da sapere. Mentre succedeva tutto questo, il daemon che non raggiungeva l’API ha concluso che ogni macchina che gestiva era morta. Si è preparato a spegnerle e riaccenderle. Quella storia è in come un limite di connessioni al database ha quasi riavviato i Mac dei miei clienti.
Domande
- Quale porta di Supabase dovrebbe usare un’app su Vercel?
- La 6543, il pooler in transaction mode. La session mode sulla 5432 tiene una connessione al server per ogni client e limita il progetto a 15, che con il serverless si esauriscono in fretta.
- Perché db.<project-ref>.supabase.co non si connette da Vercel?
- L’host diretto ha solo un indirizzo IPv6, a meno che tu non paghi l’add-on IPv4. Le funzioni Vercel non lo raggiungono via IPv6. Usa l’host del pooler, aws-0-<region>.pooler.supabase.com.
- Che username richiede il pooler?
- postgres.<project-ref>, con il project ref aggiunto in fondo. Il semplice postgres funziona solo sull’host diretto.
- Mi serve ?pgbouncer=true nella stringa di connessione?
- Con Prisma, sì. Con postgres.js, imposta invece prepare: false nelle opzioni del client. La transaction mode non supporta i prepared statement.
- Che dimensione di pool usare per ogni modalità?
- Session mode: una o due connessioni per istanza con un idle timeout breve, perché il limite è quindici. Transaction mode: un pool normale, da cinque a dieci, perché le connessioni vengono restituite dopo ogni transazione.
Fai i tuoi conti con il calcolatore oppure noleggia un runner.