Statement timeout: как быстро остановить убегающие запросы базы данных
Узнайте, как statement timeout останавливает убегающие запросы до того, как они исчерпают соединения. Установите разумные лимиты, отменяйте застрявшие задачи и сохраняйте стабильность приложения.

Почему убегающие запросы рушат в остальном нормальные приложения
Убегающий запрос — это запрос к базе данных, который выполняется намного дольше, чем вы ожидаете. Это может быть отсутствие индекса, фильтр, заставляющий сканировать всю таблицу, или джоин, который раздувается до миллионов строк. Остальная часть приложения может быть здорова, но один такой запрос держит соединение занятым до завершения.
В типичном веб‑приложении каждый запрос берёт соединение из ограниченного пула. Если медленный запрос занимает одно соединение на 30–120 секунд, несколько пользователей, попавших на один и тот же эндпойнт, могут забрать все доступные соединения. Как только пул пуст, даже быстрые запросы не получают соединение, поэтому они встают в очередь, тайм-аутятся или падают.
Симптомы выглядят как полный коллапс, хотя виноват лишь один запрос: страницы виснут и потом падают, латентность растёт по всему сайту, увеличиваются 500‑ки из‑за блокировок воркеров, фоновые задания накапливаются, CPU базы данных скачет, а пропускная способность падает.
Это часто проявляется сразу после деплоя прототипа, сгенерированного AI. Многие инструменты AI делают рабочий демо‑вариант, который не учитывает продакшен‑детали вроде индексов, безопасных паттернов запросов и лимитов. Функция, которая «работала» на 200 строках, может рухнуть на 200 000.
Конкретный пример: страница поиска добавляет гибкий фильтр вроде "status contains" или "name starts with". В продакшене это превращается в шаблон‑совпадение по большой таблице. Один человек экспортирует результаты, запрос работает минуту, и ещё пять человек делают то же самое. Внезапно пул соединений исчерпан и остальная часть приложения выглядит недоступной.
Statement timeout важен потому, что он ставит жёсткий потолок на ущерб, который может причинить один плохой запрос.
Statement timeouts: что они делают и чего не делают
Statement timeout — это предел времени, который база данных выделяет на выполнение одного SQL‑запроса. Если запрос превышает этот предел, база останавливает его и возвращает ошибку вместо того, чтобы продолжать жрать CPU, держать блокировки и занимать соединение.
Это отличается от таймаутов вне базы данных. Таймаут в приложении или балансировщике может перестать ждать, но сам запрос может продолжать выполняться в базе.
Практически:
- Таймауты на уровне базы (statement timeouts) останавливают работу SQL на сервере.
- Таймауты запросов в приложении прекращают ожидание, но база может всё ещё быть занята.
- Таймауты балансировщика разрывают сетевой запрос; база может продолжать работать, если запрос не был отменён.
Когда долгий запрос убивают, база освобождает то, что может, но поведение зависит от контекста. Если запрос держал блокировки, они освободятся после отмены и отката. Если вы были внутри транзакции, многие СУБД пометят транзакцию как провалившуюся — придётся откатить её перед дальнейшей работой на том же соединении. Это важно, потому что таймаут может «отравить» соединение до тех пор, пока вы его не почистите.
На стороне приложения вы обычно увидите ошибку вроде «query canceled» или «statement timeout». Относитесь к этому как к обычному ожидаемому падению: поймайте ошибку, откатите транзакцию при необходимости и решите, стоит ли повторять операцию. Не повторяйте автоматически тот же медленный запрос без изменений.
При грамотном использовании statement timeouts не дадут одному плохому запросу превратить это в исчерпание пула соединений.
Где накладывать таймауты: в базе, в приложении или и там, и там
Используйте оба уровня, когда можете. Лимит на стороне базы останавливает убегающую работу, даже если приложение застряло, а лимит на стороне приложения держит серверы отзывчивыми и освобождает потоки обработки.
Таймауты на стороне базы (жёсткая остановка)
Задайте statement timeout в базе как страховочный поручень. Если запрос выполняется дольше допустимого, база его отменяет — это защищает общие ресурсы: CPU, блокировки и соединения.
Практический подход — поставить разумный глобальный дефолт, а уж расширять его только для ролей или задач, которые действительно этого требуют. Многие команды используют:
- Глобальный дефолт для обычного трафика приложения
- Большее ограничение для роли админа, используемой для поддержки и бэкфиллов
- Отдельную роль для отчётов с большим бюджетом времени
Также решите, какой охват нужен. Таймаут можно применить на соединение (покрывает всё в этом соединении) или на транзакцию (полезно, когда хотите более жёсткие пределы для конкретного рабочего потока).
Таймауты на стороне приложения (защита UX)
У приложения тоже должен быть свой дедлайн, чтобы запросы не висели в ожидании базы. Когда приложение таймаутится, оно должно отменить запрос в базе и вернуть понятное сообщение пользователю, например: «Этот поиск занял слишком много времени. Попробуйте сузить фильтры.»
Для особых случаев используйте переопределения на уровне конкретного запроса, а не смягчайте общие настройки. Держите такие пути явными: долгие миграции, одноразовые исправления данных или ежемесячные отчёты.
Вот отказ, которого вы пытаетесь избежать: новый фильтр "contains" вызывает медленное сканирование большой таблицы. Без лимитов 20 пользователей нажмут его, все соединения застрянут, и всё приложение будет казаться упавшим. С таймаутами в базе и отменой со стороны приложения эти запросы быстро падают, пул восстанавливается, и вы можете безопасно исправить запрос.
Выберите таймаут, соответствующий реальным нагрузкам
Таймаут должен защищать приложение, не ломая нормальный трафик. Самая лёгкая ошибка — выбрать число интуитивно. Берите значения из реальных метрик пользователей и добавляйте небольшие запасные буферы.
Начните с реальных p95 и p99
Соберите длительности запросов из логов или статистики базы и сгруппируйте их по эндпойнтам или типам задач. Если типичный API‑запрос завершается за 120 мс на p95 и 400 мс на p99, хватит ограничения в 2–3 секунды. Оно ловит редкие убегающие запросы и даёт обычным всплескам пространство для дыхания.
Если данных пока нет — начните консервативно и ужесточайте по мере получения распределений.
Используйте разные лимиты для разных типов работы
У большинства приложений есть как минимум три класса работы с БД, и у них не должно быть одного потолка:
- Пользовательские API‑запросы: короткие, строгие лимиты
- Фоновые задания: более длинные лимиты, но всё равно ограниченные
- Админские и отчётные интерфейсы: самые длинные лимиты, но за доступом и пагинацией
Держите правила простыми. Если отчёт требует 30 секунд — ок, но не запускайте его при тех же настройках, что логин или корзина.
Разрешайте исключения, но с ограждениями
Некоторые операции по определению тяжёлые: бэкфиллы, экспорты, отчёты по концу месяца. Давайте им явные большие таймауты, но требуйте ограждений: фильтры, диапазоны дат, пагинацию или максимум строк.
Пример: отчёт «Все клиенты» без фильтра по дате работает в стейджинге, но в проде идёт минуты и блокирует соединения. Больший таймаут для отчётов плюс обязательный диапазон дат предотвращают такой сценарий.
Пошагово: добавьте таймауты запросов и отмену
Начните с защиты самой базы. Безопасный дефолт значит, что один плохой запрос не будет сидеть вечно и блокировать соединения. В Postgres это обычно statement_timeout, установленный на уровне базы или роли, чтобы он действовал, даже если разработчик забыл добавить таймаут в коде.
-- Example: Postgres
ALTER ROLE app_user SET statement_timeout = '5s';
-- Or for a whole database
ALTER DATABASE app_db SET statement_timeout = '5s';
Дальше добавьте более жёсткий таймаут для пользовательских действий в приложении. Человек, нажимающий кнопку, ожидает быстрый ответ. Если запрос попадает в таймаут, быстро падать с понятным сообщением и дать возможность повторить позже — лучше, чем ждать и постепенно исчерпать пул соединений.
Для фоновой работы (воркеры, cron) используйте другой таймаут. Задания часто обрабатывают больше строк, поэтому им можно дать больше времени, но жёсткий потолок нужен, чтобы одно застревание не блокировало всю очередь.
Последовательность, которую легко удерживать:
- Установите дефолтный таймаут на уровне базы или роли, который безопасен, но не идеален.
- Применяйте таймауты на уровне запроса для веб‑ и API‑эндпойнтов.
- Применяйте таймауты на уровне задач для воркеров и планировщика задач.
- Используйте переопределения на уровне запроса только когда можете объяснить причину (например, ежемесячный отчёт).
- Проверяйте в стейджинге с реалистичным объёмом данных и конкуренцией.
Переопределения на уровне запроса — место, где команды ошибаются. Относитесь к ним как к исключениям: логируйте их использование, ограничивайте область (устанавливайте только для нужной транзакции, затем сбрасывайте) и регулярно пересматривайте.
Наконец, тестируйте не только тайминги, но и поведение при отмене. В стейджинге выполните преднамеренно медленный запрос (например, фильтр без индекса) и проверьте три вещи: запрос завершается, запрос останавливается в базе, и соединение возвращается в пул.
Сделайте отмену надёжной со стороны приложения
Таймаут помогает только если он действительно освобождает соединение. Установите дедлайн на уровне границы приложения (HTTP‑хендлер, раннер задач, воркер) и прокиньте его до вызова базы. Тогда когда запрос завершился, запрос в базе тоже должен завершиться.
Первая ловушка — поведение драйвера. Некоторые драйверы просто перестают ждать результата, но база продолжает выполнять запрос. В проде это почти так же плохо, как отсутствие таймаута, потому что соединение остаётся занятым. Протестируйте стек: форсируйте медленный запрос и убедитесь в двух вещах — приложение отвечает быстро, и в базе видно, что запрос был отменён (а не продолжает выполняться в фоне).
Когда отменяете, возвращайте пользователю понятное сообщение и код, с которым ваш код может работать. «Этот запрос занял слишком много времени, попробуйте ещё раз» обычно достаточно. Также отделяйте ошибки таймаута от реальных ошибок (синтаксические, права), чтобы мониторинг и повторные попытки были честными.
Повторы требуют правил. Иначе они удваивают нагрузку во время инцидента:
- Повторяйте только операции чтения, и только если это безопасно и вы не начали стримить ответ.
- Не повторяйте записи без идемпотентных ключей или стратегии «ровно один раз».
- Добавьте джиттер и небольшой лимит (например, 1–2 повтора), не бесконечные попытки.
- Никогда не повторяйте на ошибки авторизации или некорректные запросы.
Логируйте достаточно, чтобы дебажить, но не лейте секреты. Захватывайте маршрут или имя задачи, значение таймаута, прошедшее время, отпечаток запроса (хэш или шаблон) и request ID. Избегайте логирования сырого SQL с пользовательскими данными, токенами или строками подключения.
Распространённые ошибки, делающие таймауты вредными
Таймауты призваны защищать приложение, но несколько настроечных ошибок могут превратить их в шумные падения или скрыть реальную проблему.
Рelying только на веб‑таймауты — классическая точка отказа. Если браузер или балансировщик сдаются через 30 секунд, запрос в базе может продолжать выполняться. Эти «сиротские» запросы держат соединения даже после ухода пользователя.
Ещё одна распространённая ошибка — ставить таймауты слишком малыми. Общий таймаут в 200 мс кажется безопасным, но может вызывать постоянные повторы, частичные страницы и тикеты в поддержку. Нужно останавливать настоящие убегающие запросы, а не наказывать нормальные медленные случаи (cold cache, крупные арендаторы, временные пики).
Транзакции — ещё одна ловушка. Таймаут внутри транзакции может оставить её в состоянии failure. Если вы не обработаете это и не откатите, можно держать блокировки и блокировать другие запросы, создавая накапливание, которое выглядит как «замерла» база.
Наконец, избегайте одного таймаута для всего. Интерактивные страницы требуют строгих лимитов, а экспорты, бэкфиллы и админ‑отчёты — иные требования. Дайте долгим задачам отдельный путь и более высокий таймаут, чтобы обычные пользователи были защищены.
Как находить главных нарушителей до того, как они вас убьют
Statement timeouts — это страховочная сетка, но вам всё равно нужно знать, какие запросы бьют в эту сетку.
Начните с поиска у места отказа. Вместо логирования каждого запроса (слишком шумно) сфокусируйтесь на медленных запросах и запросах «вблизи таймаута». Многие базы умеют логировать запросы дольше порога, также полезно сэмплировать запросы, которые выполняются дольше 70–90% от вашего таймаута. Этот срез часто показывает те же паттерны, которые потом вызывают аутейджи.
Следите за двумя сигналами на уровне приложения вместе с логами базы: как часто запросы отменяются и не заполнен ли пул соединений. Растущее число отмен и пул, висевший у максимума — значит таймауты предотвращают крах, но еле‑еле.
Отслеживайте постоянно и сигнализируйте, когда показатели высоки несколько минут:
- Медленные запросы выше фиксированного порога (и отдельный счётчик для «вблизи таймаута»)
- Количество отменённых запросов (по эндпойнту или типу задания)
- Использование пула соединений и время ожидания свободного соединения
- Уровень ошибок и p95 латентности для эндпойнтов, обращающихся в БД
- Топ отпечатков запросов (одна форма, разные параметры)
Когда сохраняете запросы для расследования, сохраняйте шаблон, а не персональные данные. Держите плейсхолдеры (WHERE email = ?) и план или использованный индекс, но избегайте логирования реального email, токенов или полного полезного груза.
Пример сценария: один медленный фильтр, который ломает приложение
Основатель запускает простую страницу поиска «Customers» с фильтрами «Company name contains …» и «Signed up after …». В тестах всё ок, потому что база маленькая.
В проде один пользователь вводит частый символ, например «a», и нажимает Enter. Приложение посылает запрос, который не может использовать индекс для фильтра «contains». База сканирует огромную таблицу, сортирует большой набор результатов и держит соединение открытым.
Цепочка отказов предсказуема:
- Один запрос работает минуты, потому что сканирует миллионы строк.
- Люди повторяют поиск, и каждый запрос захватывает ещё одно соединение.
- Пул заполняется, поэтому даже быстрые эндпойнты (логин, оплата, админ) начинают тайм-аутиться.
- Приложение выглядит упавшим, но реальная проблема — несколько убегающих запросов.
С statement timeout и отменой со стороны приложения можно задать лимит, соответствующий UX, например 3–10 секунд для страницы поиска. Когда запрос достигает лимита, база его останавливает. Запрос быстро падает с понятным сообщением, и соединение возвращается в пул.
Ключевое преимущество в том, что один плохой запрос не может пожирать ресурсы настолько, чтобы задушить всё остальное.
После тушения пожара исправьте паттерн: добавьте правильный индекс, измените фильтр на индексируемый вариант или вынесите «contains» в отдельную колонку для поиска.
Быстрая чек‑лист перед релизом
Перед деплоем пройдитесь ещё раз, чтобы один плохой запрос не мог заблокировать пул и не положить приложение.
- Установите разумный дефолт statement timeout для обычных путей. Он должен быть достаточно низким, чтобы защищать систему, и достаточно высоким, чтобы нормальные страницы не падали.
- Используйте отдельный, более длинный таймаут для доверенных задач вроде экспортов и отчётов, привязанный к соответствующим эндпойнтам или воркерам.
- Подтвердите, что дедлайн приложения и таймаут базы работают вместе, и что отмена действительно срабатывает. Когда запрос прерывается, запрос должен остановиться и соединение быстро вернуться в пул.
- Обрабатывайте ошибки таймаута аккуратно (понятное сообщение, безопасный код) и избегайте циклов повторов, которые снова запускают тот же тяжёлый запрос.
- Мониторьте запросы вблизи таймаута и отменённые запросы, чтобы исправлять худших нарушителей раньше.
Быстрая проверка реальности: запустите намеренно медленный запрос (например, отчёт с широким диапазоном дат), затем отмените его в браузере. Наблюдайте за активностью базы и логами приложения. Если запрос продолжает работать после исчезновения запроса, у вас ещё есть разрыв в отмене.
Следующие шаги: стабилизируйте базу без полного переписывания
Если вы уже видели, как один убегающий запрос убил приложение, воспринимайте это как задачу по безопасности. Цель — оставить систему работоспособной, даже когда запрос медленный, фильтр слишком широк или задача застряла.
Начните с аудита мест, где время может ускользнуть: эндпойнты с множеством опциональных фильтров, отчёты, сканирующие большие диапазоны дат, фоновые задания с фэн‑аутах и всё, что запускается по расписанию. Затем усили один реальный рабочий поток end‑to‑end, например дашборд на логине. Дайте ему реалистичный таймаут, убедитесь, что отмена чистая, и что медленный запрос не может блокировать пул.
Если вы унаследовали код, сгенерированный AI, предполагайте наличие скрытых ловушек, пока не доказано обратное. Две частые — N+1 запросы (цикл, который тихо выполняет сотни мелких запросов) и неограниченные фильтры (пустой поиск, возвращающий всю таблицу).
Если хотите посторонний взгляд на прототип, который ломается под реальным трафиком, FixMyMess (fixmymess.ai) специализируется на превращении AI‑сгенерированных приложений в готовый к продакшен софт: диагностика медленных путей, исправление логики и повышение безопасности.