Работа с базой из Node.js: 7 ошибок, которые роняют прод
Утечки соединений, забытый await, SQL-инъекции через шаблонные строки и таймауты пула — разбираем ошибки при работе с PostgreSQL и MySQL из Node.js и как их не допустить.

Сервис на Node.js держит нагрузку в разработке, а на проде через час выдаёт Error: timeout exceeded when trying to connect или молча копит открытые соединения, пока база не упрётся в лимит. Причина почти всегда в том, как код обращается к базе: соединения не возвращаются в пул, запросы собираются конкатенацией строк, а асинхронные вызовы теряются без await. Разбираем типичные приёмы, которые дают эти проблемы, и что писать вместо них.
Примеры даны для драйверов pg (node-postgres 8.x) и mysql2 (3.x) на Node.js 20.x. Подходы одинаковы для обеих СУБД; различается синтаксис плейсхолдеров и API пула.
Ошибка 1. Одно соединение вместо пула
Частый старт: создать один Client, подключиться и гонять через него все запросы приложения.
// Плохо: один клиент на всё приложение
const { Client } = require('pg');
const client = new Client({ connectionString: process.env.DATABASE_URL });
client.connect();
app.get('/users', async (req, res) => {
const result = await client.query('SELECT id, name FROM users');
res.json(result.rows);
});
Одно соединение обрабатывает запросы строго по очереди. При параллельных HTTP-запросах они выстраиваются в очередь на уровне драйвера, а если соединение оборвётся — отвалится всё приложение сразу. Нужен пул: набор переиспользуемых соединений.
// Хорошо: пул соединений
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10, // максимум соединений
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
app.get('/users', async (req, res) => {
// pool.query сам берёт соединение и возвращает его
const result = await pool.query('SELECT id, name FROM users');
res.json(result.rows);
});
Метод pool.query() берёт свободное соединение, выполняет запрос и возвращает соединение в пул автоматически. Это самый безопасный способ для одиночных запросов.
Ошибка 2. Взяли соединение из пула и не вернули
Проблема появляется, когда нужен не один запрос, а несколько подряд на одном соединении — например, транзакция. Тогда берут соединение через pool.connect(). И забывают release().
// Плохо: соединение утекает при ошибке
const clientConn = await pool.connect();
await clientConn.query('BEGIN');
await clientConn.query('UPDATE accounts SET balance = balance - 100 WHERE id = 1');
await clientConn.query('UPDATE accounts SET balance = balance + 100 WHERE id = 2');
await clientConn.query('COMMIT');
clientConn.release(); // если выше кинуло исключение — сюда не дойдём
Если любой запрос между connect() и release() бросит исключение, соединение никогда не вернётся в пул. Несколько таких ошибок — и пул исчерпан, все новые запросы висят в ожидании до connectionTimeoutMillis, потом падают. Возврат делается только через finally.
// Хорошо: release в finally, откат при ошибке
async function transferMoney(from, to, amount) {
const conn = await pool.connect();
try {
await conn.query('BEGIN');
await conn.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, from]);
await conn.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, to]);
await conn.query('COMMIT');
} catch (err) {
await conn.query('ROLLBACK');
throw err;
} finally {
conn.release(); // выполнится в любом случае
}
}
Утечка соединений не даёт симптома сразу. Приложение работает, пул медленно пустеет, и падение приходит через час под нагрузкой — когда воспроизвести на локальной машине уже невозможно.
Ошибка 3. SQL-инъекция через шаблонные строки
Самая опасная ошибка. Значение из запроса подставляют прямо в текст SQL.
// Уязвимо: значение из URL попадает в SQL напрямую
const email = req.query.email;
const result = await pool.query(
`SELECT * FROM users WHERE email = '${email}'`
);
Если прислать email=' OR '1'='1, запрос вернёт всю таблицу. Более злые варианты позволяют дропнуть таблицу или вытащить чужие данные. Решение — параметризованные запросы: значение передаётся отдельно от текста, драйвер экранирует его сам.
// Безопасно: $1 — плейсхолдер, значение передаётся вторым аргументом
const result = await pool.query(
'SELECT * FROM users WHERE email = $1',
[req.query.email]
);
В pg плейсхолдеры нумерованные ($1, $2), в mysql2 — знаки вопроса (?). Никогда не собирайте текст запроса конкатенацией с пользовательскими данными, даже если «это же только внутренний ID».
Ошибка 4. Забытый await
Все методы драйвера асинхронные. Пропущенный await не вызывает ошибку компиляции, но ломает логику.
// Плохо: await пропущен
async function createUser(name) {
pool.query('INSERT INTO users(name) VALUES ($1)', [name]); // нет await
return 'created'; // вернётся до того, как запрос выполнится
}
Функция вернёт 'created', не дождавшись вставки. Если запрос упадёт — исключение станет необработанным промисом (unhandledRejection), а не попадёт в ваш try/catch. В транзакции пропущенный await ломает порядок: COMMIT может уйти раньше UPDATE. Включите ESLint-правило, которое ловит проигнорированные промисы, — оно надёжнее ручного контроля.
Ошибка 5. Нет обработчика ошибок на пуле
Соединение в пуле может умереть, пока оно простаивает: база перезапустилась, сеть моргнула, сервер закрыл idle-соединение по своему таймауту. Драйвер эмитит событие error на пуле. Если его никто не слушает, Node.js уронит процесс целиком.
pool.on('error', (err) => {
// логируем, но не падаем — пул сам заменит битое соединение
console.error('Неожиданная ошибка на idle-клиенте', err);
});
Это одна строка, которую забывают в 9 проектах из 10, пока прод не перезапустит базу в первый раз.
Ошибка 6. Размер пула наугад
Значение max часто ставят «на глаз»: 100, чтобы «хватило». Но каждое соединение — это процесс или поток на стороне базы и память под него. У PostgreSQL по умолчанию max_connections порядка 100 на весь сервер. Если четыре инстанса приложения открывают по 100 соединений — база откажет в новых.
Считайте от лимита базы, а не от нагрузки приложения:
- Узнайте лимит соединений базы (в PostgreSQL —
SHOW max_connections;). - Вычтите резерв под служебные подключения и миграции.
- Разделите на число инстансов приложения, которые ходят в эту базу.
- Полученное число — верхняя граница
maxна инстанс.
Большой пул не ускоряет работу: если база не успевает обрабатывать запросы, лишние соединения только увеличивают конкуренцию за её ресурсы. Часто 10–20 соединений на инстанс работают быстрее, чем 100.
Ошибка 7. Запрос без таймаута
Запрос по умолчанию ждёт ответа сколько угодно. Один тяжёлый или зависший запрос удержит соединение, за ним — второй, и пул исчерпается. Ограничивайте время выполнения на уровне запроса.
// pg: таймаут на выполнение конкретного запроса (мс)
const result = await pool.query({
text: 'SELECT ... FROM big_table WHERE ...',
values: [id],
// прервать запрос, если он идёт дольше 5 секунд
// (statement_timeout задаётся на сервере или через SET)
});
Точное имя параметра таймаута зависит от драйвера и версии: в node-postgres время выполнения ограничивается серверным statement_timeout либо параметрами пула. Сверьтесь с актуальной документацией pg и mysql2 для вашей версии, прежде чем полагаться на конкретное имя ключа, — API таймаутов у драйверов различается.
Сводка: что искать в своём коде
| Симптом на проде | Вероятная причина | Что проверить |
|---|---|---|
| timeout при подключении, пул пуст | Утечка соединений | release() в finally |
| Данные не сохранились, ошибки нет | Пропущен await | ESLint на промисы |
| Процесс падает после рестарта БД | Нет pool.on('error') | Обработчик события error |
| База отказывает в соединениях | Слишком большой max | Лимит БД / число инстансов |
| Утечка чужих данных | SQL-инъекция | Плейсхолдеры вместо конкатенации |
Что делать дальше
Соберите чек-лист для ревью: параметризация запросов, release() в finally, обработчик error на пуле, осмысленный max. Настройте мониторинг числа активных и ожидающих соединений — у pg пул отдаёт pool.totalCount, pool.idleCount, pool.waitingCount. Рост waitingCount под нагрузкой — первый сигнал, что пул мал или соединения утекают. Отдельная тема — пулинг на стороне инфраструктуры (PgBouncer) для многоинстансных развёртываний; здесь мы разбирали только уровень приложения.
Проектная работа: От проблемы до защиты: продукт, этапы, вопросы комиссии
Спросить за 15 ₽Источники: node-postgres — Pooling (официальная документация), mysql2 — документация пакета на npm
Частые вопросы
Использовать pool.query или pool.connect?
А если у меня mysql2, а не pg?
Не сработало — соединения всё равно утекают, хотя release есть
Нужен ли ORM, чтобы избежать этих ошибок?
Какой max ставить для пула на проде?
Как поймать пропущенный await до прода?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.