Postgres FATAL: too many connections — как лечить
Postgres падает не от количества SQL-запросов и не от размера базы. Чаще всего у людей в проде он начинает выдавать вот это:
FATAL: sorry, too many clients alreadyСообщение прямое: достигнут лимит max_connections. Сервис, который попытался открыть соединение, не получает его и крашится. На пять минут позже уже половина бэкенда не работает, потому что новые поды/процессы тоже не могут подключиться.
В моём опыте корень проблемы почти никогда не в самом Postgres. Он в том, что приложение открывает много коннектов, не закрывает их, или пытается обращаться к БД из десятков параллельных воркеров без пула.
Что выставлено по умолчанию
Дефолт Postgres — max_connections = 100. На managed-хостингах часто чуть выше, но в любом случае это не «бесконечно». Проверить:
SHOW max_connections;Сколько сейчас открыто и кто открыл — главный запрос для диагностики:
SELECT datname, usename, application_name, state, count(*)
FROM pg_stat_activity
GROUP BY datname, usename, application_name, state
ORDER BY count DESC;В реальной жизни этот вывод чаще всего показывает картину типа: 80 idle-коннектов от одного приложения и 5 активных. Сразу видно: connection pool настроен криво или его нет вовсе.
Не поднимай max_connections наугад
Соблазн — увеличить лимит. Сделать max_connections = 500 и забыть. Так нельзя.
Каждое соединение в Postgres — это отдельный процесс ОС с собственным work_mem и стеком. На каждое уходит несколько мегабайт ОЗУ ещё до того, как пошли запросы. На typical-конфиге 4 ГБ ОЗУ удерживать 500 коннектов — это просто не работает: Postgres начнёт OOM-иться.
Ориентир: max_connections * (work_mem * сложные_запросы) + shared_buffers + накладные — должно влезать в ОЗУ с запасом. Для большинства небольших проектов 100–200 — потолок.
Правильное лечение: пул на стороне клиента
node-postgres / pg
Если ты создаёшь новое new Client() на каждый запрос и забываешь закрыть — соединения утекают. Используй Pool:
import { Pool } from 'pg';
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 10_000,
connectionTimeoutMillis: 5_000,
});Pool сам открывает до max коннектов, переиспользует их, закрывает простаивающие через idleTimeoutMillis. На запрос делаешь pool.query(...) — он берёт коннект из пула и возвращает его обратно автоматически.
Drizzle на основе pg
То же самое — Drizzle принимает pool снаружи:
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
});
export const db = drizzle(pool);Несколько процессов
Главная ловушка — у тебя 10 worker-процессов, и каждый держит пул на 10 коннектов. Итого 100 коннектов, и это в норме, на пиках — больше. Если на сервере несколько таких приложений, на пиках упёрся в max_connections.
Считай по формуле workers * pool.max ≤ max_connections - резерв. Резерв — на репликацию, бэкапы, миграции, мониторинг.
Когда мало клиентских пулов — ставь PgBouncer
Если приложений много (микросервисы, serverless-функции, несколько серверов), каждое со своим пулом — лимит съедается мгновенно. Решение — PgBouncer между приложениями и Postgres. Он сам держит подключения к Postgres и раздаёт их сотням клиентов.
Простой конфиг pgbouncer.ini для transaction-mode:
[databases]
app = host=127.0.0.1 port=5432 dbname=app
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25В этом сетапе приложения подключаются к 6432 (PgBouncer), а тот держит максимум 25 коннектов к самому Postgres. Тысяча клиентов — без проблем.
Подвох: в transaction-режиме нельзя использовать prepared statements между транзакциями и сессионные настройки. На большинстве веб-приложений это не проблема, но если у тебя SET LOCAL или подготовленные запросы — выбирай session-mode и считай лимиты с учётом этого.
Что делать, если уже горит
Сервис лёг, в логах too many clients. Быстро — найти и убить idle-коннекты:
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle'
AND state_change < now() - interval '5 minutes'
AND pid <> pg_backend_pid();Это освободит места в лимите. Дальше уже разбираешься, кто их открыл.
В порядке работы добавь в Postgres idle_in_transaction_session_timeout, чтобы зависшие транзакции не удерживали коннекты бесконечно:
ALTER SYSTEM SET idle_in_transaction_session_timeout = '5min';
SELECT pg_reload_conf();Это разорвёт коннекты, которые открыли транзакцию и забыли закоммитить (например, из-за бага в приложении).
Особенно опасный сценарий: serverless
Каждый холодный старт serverless-функции открывает новый коннект. На пиках их сотни. Без пула где-то посередине Postgres ляжет.
Тут есть несколько подходов:
- PgBouncer перед БД (классика);
- специальные коннекторы — RDS Proxy, Neon serverless driver, Supabase pooler;
- если БД совсем рядом и трафик невелик — переиспользовать коннект между инвокациями (хранить pool в module scope, контейнер запоминает).
Серверлесс с обычным new Client() в каждой функции — путь к too many connections за две недели роста.
Чек-лист, по которому я разбираю инцидент
- Сколько коннектов сейчас?
SELECT count(*) FROM pg_stat_activity; - Сколько из них idle, сколько активных, кто application_name?
- Соответствует ли
workers * pool.maxлимиту? - Стоит ли где-то PgBouncer? Если нет — пора.
- Не висят ли idle-in-transaction коннекты? Включить timeout.
Поднимать max_connections можно, но только осознанно: считать память, мерить, не делать с потолка. Реальное лечение — пул и контроль за тем, кто и когда открывает коннект к БД.