Состояние idle in transaction — частая причина «внезапных» деградаций PostgreSQL: растёт задержка запросов, autovacuum перестаёт нормально убирать мусор, накапливается bloat, а иногда всё упирается в блокировки и цепочки ожидания. Снаружи при этом кажется, что нагрузки нет: сессия просто «сидит» и ничего не делает.
Почти всегда это означает одно: клиент открыл транзакцию (явно или неявно), выполнил одну или несколько команд и перестал отправлять запросы, не завершив транзакцию COMMIT/ROLLBACK. PostgreSQL обязан сохранять согласованность так, как будто транзакция ещё может продолжиться.
Что значит idle in transaction и почему это опасно
Пока транзакция не завершена, сервер вынужден удерживать контекст, который влияет на систему целиком. В частности:
- держит снимок видимости данных (snapshot) для MVCC;
- может удерживать блокировки на затронутых объектах (в зависимости от выполненных команд);
- мешает vacuum удалять версии строк, которые потенциально видимы этой транзакции;
- иногда удерживает ресурсы вроде курсоров, временных объектов и т.д.
Самая токсичная часть — старый snapshot. Даже если тяжёлых блокировок нет, autovacuum не сможет вычистить «мёртвые» версии строк, пока существует транзакция, которая теоретически может их увидеть. На практике это приводит к цепочке последствий:
- растёт bloat таблиц и индексов;
- ухудшается производительность запросов из-за лишних чтений и разрастания индексов;
- повышается риск проблем с заморозкой XID (wraparound), если долго не удаётся продвинуть «горизонт»;
- на репликах (hot standby) возможны конфликты из-за старых снимков.
Типичная картина: «всё было нормально, потом стало медленно», а истинная причина — одна зависшая транзакция, из-за которой vacuum не может убирать мусор в горячих таблицах.
Как найти idle in transaction через pg_stat_activity
Основной «фонарик» — pg_stat_activity. Он показывает текущие состояния сессий, возраст транзакций и последний выполненный запрос. Начните с прямого списка «простои в транзакции» и сортировки по возрасту.
Быстрый список «подозрительных» сессий
SELECT
pid,
usename,
application_name,
client_addr,
state,
now() - xact_start AS xact_age,
now() - state_change AS state_age,
wait_event_type,
wait_event,
left(query, 200) AS query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY xact_age DESC;
Что смотреть в первую очередь:
xact_age— возраст транзакции. Чем он больше, тем выше шанс проблем с MVCC/vacuum.state_age— как давно сессия именно «простаивает». Иногда транзакция старая, но работа в ней активная; нас интересует именно простой.application_name— если вы задаёте его в приложении/пуле, поиск источника занимает секунды.client_addr— помогает отличить сервисы от «ручных» подключений (IDE, psql, админские машины).query— последняя команда. Часто этоBEGINили запрос перед тем, как код «ушёл ждать».
Long transactions: проблемы не только у idle
Отдельный класс — просто очень долгие транзакции, которые не обязательно находятся в idle in transaction. Например: длинные отчёты на REPEATABLE READ или массовые изменения без батчинга. Для общего обзора полезно посмотреть топ по возрасту транзакций:
SELECT
pid,
usename,
application_name,
client_addr,
state,
now() - xact_start AS xact_age,
left(query, 200) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_age DESC
LIMIT 30;
Если вверху списка в основном active, это уже история про оптимизацию запросов, батчинг, уровни изоляции и сопутствующие таймауты.
Если вы держите PostgreSQL на отдельном сервере, удобнее и предсказуемее управлять CPU/RAM и дисковой подсистемой на VDS, чем в общем окружении.
Как понять, мешает ли idle in transaction прямо сейчас
Вопрос «Ну висит сессия, и что?» решается быстрой оценкой по трём направлениям: блокировки, влияние на vacuum и признаки накопления мусора. Не обязательно сразу строить сложные отчёты — на инциденте важна скорость.
Проверяем признаки ожиданий и блокировок
Если транзакция удерживает блокировки, другие сессии будут ждать. Быстрый способ увидеть «кто вообще ждёт чего-то» — посмотреть активные ожидания:
SELECT
pid,
usename,
application_name,
client_addr,
state,
now() - xact_start AS xact_age,
now() - state_change AS state_age,
wait_event_type,
wait_event,
left(query, 200) AS query
FROM pg_stat_activity
WHERE wait_event_type IS NOT NULL
ORDER BY xact_age DESC;
Если много сессий ждут, а среди «старых» транзакций есть idle in transaction, это частый кандидат на роль пробки. Для полноценной связки «кто кого блокирует» обычно подключают pg_locks, но даже этот запрос часто даёт направление.
Проверяем влияние на vacuum косвенно: dead tuples
Удержание snapshot напрямую бьёт по уборке мусора. Косвенно это видно по росту n_dead_tup и стагнации показателей vacuum. Быстро посмотрите топ таблиц по «мёртвым» строкам:
SELECT
schemaname,
relname,
n_live_tup,
n_dead_tup,
last_autovacuum,
last_vacuum,
vacuum_count,
autovacuum_count
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;
Если n_dead_tup заметно растёт, а autovacuum «ходит», но эффекта нет, очень часто причина — именно старая транзакция со snapshot (в том числе idle in transaction).
Если тема autovacuum в целом болит (параметры, пороги, freeze, индексы), держите под рукой разбор: настройка autovacuum и борьба с bloat в PostgreSQL.

Почему это происходит: типовые причины
В проде причины повторяются, и почти всегда это дисциплина транзакций на стороне клиента.
- Пул соединений + неправильный жизненный цикл транзакции. Взяли коннект, сделали
BEGIN, выполнили запрос и ушли ждать внешний I/O (HTTP, очередь, таймер), не закрыв транзакцию. - «Ручной» клиент (psql, DBeaver, DataGrip): человек открыл транзакцию, посмотрел данные, отвлёкся, ноутбук уснул.
- ORM и автотранзакции. Транзакция живёт «на запрос» (request-scope), а обработчик завис или делает долгий I/O.
- Отключён autocommit и «неочевидные»
BEGINпоявляются без явного намерения. - Курсоры и долго живущие сессии, которые держат контекст дольше, чем ожидалось.
Админская мораль простая: транзакция должна быть короткой либо контролируемой и наблюдаемой. «Пусть повисит» для OLTP обычно заканчивается деградацией всей базы.
Как лечить: от мягких мер к жёстким
Лечение стройте по принципу минимального вмешательства, но с понятным дедлайном. Сессию всегда можно завершить, но лучше сначала понять, что вы прерываете и как это отразится на приложении.
Шаг 1. Найти владельца и контекст
Сопоставьте application_name, usename, client_addr. Если это «человек» — часто достаточно попросить завершить транзакцию (commit/rollback) или просто закрыть клиент. Если это сервис — фиксируйте точное имя воркера/пула и время, чтобы разработчики могли найти место в логах.
Шаг 2. Аккуратно прервать сессию
Если сессия простаивает, активного запроса нет, поэтому pg_cancel_backend обычно не поможет: отменять нечего. В таком случае применяется завершение бэкенда:
SELECT pg_terminate_backend(12345);
Эффект: соединение разорвётся, транзакция откатится. Для приложения это выглядит как ошибка соединения/транзакции, и корректный код должен уметь повторить операцию (учитывая идемпотентность и бизнес-логику).
Не делайте terminate «первой реакцией». Но и не тяните бесконечно: одна зависшая транзакция может стоить дороже, чем один откат и ретрай.
Шаг 3. Включить idle_in_transaction_session_timeout
Главный предохранитель — idle_in_transaction_session_timeout. Он завершает сессию, которая слишком долго находится в состоянии idle in transaction. Это лучшее средство профилактики повторений из-за человеческого фактора или редких багов.
Проверить текущее значение:
SHOW idle_in_transaction_session_timeout;
Задать на уровне базы (пример — 2 минуты):
ALTER DATABASE yourdb SET idle_in_transaction_session_timeout = '2min';
Задать на уровне роли (часто самый безопасный старт — только роль приложения):
ALTER ROLE appuser SET idle_in_transaction_session_timeout = '2min';
Или глобально через ALTER SYSTEM (делайте так, только если понимаете последствия для всех ролей):
ALTER SYSTEM SET idle_in_transaction_session_timeout = '2min';
SELECT pg_reload_conf();
Как выбрать значение:
- Для типичного веб-OLTP часто достаточно 30–120 секунд.
- Если есть легаси-участки, где транзакции «думают», начните с 5–10 минут и снижайте постепенно.
- Разным ролям можно задать разные лимиты: приложению меньше, админам/аналитике больше.
Если приложение работает на виртуальном хостинге и упирается в ограничения окружения, иногда проще вынести базу на отдельный VDS и уже там аккуратно настроить лимиты, мониторинг и дисковую подсистему.
Как не сломать приложение таймаутом: практические рекомендации
Главная ошибка — включить idle_in_transaction_session_timeout «везде и сразу», а потом ловить непонятные обрывы транзакций в пике. Внедряйте управляемо.
Включайте по ролям и наблюдайте
Начните с роли приложения и смотрите, где всплывают ошибки. Если у вас несколько сервисов ходят в одну базу — разделяйте роли и выставляйте разные значения. Так вы локализуете последствия и не сломаете админские операции.
Убедитесь, что ретраи безопасны
После разрыва соединения приложение получит ошибку и должно корректно повторить транзакцию. Но ретраи опасны для неидемпотентных операций (условно «списать деньги»). Админу полезно проговорить это с разработчиками и предложить ключи идемпотентности там, где это критично.
Не держите транзакцию во время внешнего I/O
Классика: в транзакции сделали SELECT ... FOR UPDATE, затем ушли во внешний API, а потом вернулись обновлять запись. На время внешнего вызова транзакция может стать idle in transaction, а блокировки и snapshot будут удерживаться. Правильнее: минимально фиксировать состояние в короткой транзакции, внешний I/O делать вне транзакции, затем выполнять вторую короткую транзакцию с повторной проверкой состояния.
Если причина в неверной конфигурации пула, часто помогает правильная настройка PgBouncer и режимов транзакций/сессий: практический гайд по PgBouncer для PostgreSQL.

Полезные дополнительные таймауты вокруг темы
idle_in_transaction_session_timeout закрывает именно простои внутри транзакции. Рядом часто уместно включать и другие ограничители, чтобы «края» были предсказуемыми:
statement_timeout— ограничивает время выполнения одного SQL-оператора (борется с «вечными» запросами).lock_timeout— ограничивает ожидание блокировки (уменьшает залипание в очередях ожидания).transaction_timeout— ограничивает общую длительность транзакции, даже если она активна (доступность зависит от версии PostgreSQL).
Практика: для API и OLTP — умеренные таймауты, для аналитики — более мягкие, но лучше в отдельном контуре (отдельные роли/пулы, а иногда и отдельная реплика для чтения).
Оперативный чек-лист для дежурного админа
- Найдите idle in transaction с максимальным
xact_ageвpg_stat_activity. - Определите источник по
application_name/client_addrи решите, можно ли дать владельцу время завершить транзакцию. - Проверьте наличие ожиданий/блокировок по
wait_event_typeи общей картине ожиданий. - Посмотрите топ таблиц по
n_dead_tupвpg_stat_user_tables. - Если риск для продакшена высокий — завершайте сессию через
pg_terminate_backend. - После инцидента включите
idle_in_transaction_session_timeout(начните с роли приложения) и зафиксируйте правила: никаких внешних I/O внутри транзакции, никаких «думающих» транзакций в OLTP.
Итоги
idle in transaction — это не «косметика» в мониторинге, а реальный источник долгосрочных проблем: bloat, деградация запросов, конфликты vacuum и блокировки. Диагностика обычно быстрая: pg_stat_activity показывает виновника по возрасту транзакции и атрибутам сессии. Лучшее лечение — профилактика: разумный idle_in_transaction_session_timeout плюс дисциплина транзакций в приложении.


