Что такое backpressure (обратное давление) — и почему скрипт съел всю память
Смотри, какой опыт я поставил. Есть приёмник, который пишет данные медленно: десять миллисекунд на кусок. Я отправил ему 200 тысяч кусков по килобайту. Подряд, в цикле, не дожидаясь ответа.
Цикл закончился мгновенно. Ошибок не было. А в памяти процесса повисли 205 мегабайт — всё, что приёмник ещё не успел переварить.
Никакого бага в строчке кода нет. Просто я проигнорировал одно слово, которое мне возвращали двести тысяч раз подряд. Это слово — false. И означает оно «притормози».
Что такое backpressure простыми словами
Backpressure (обратное давление) — это сигнал от того, кто получает данные, тому, кто их отправляет: «я не успеваю, подожди».
Представь конвейер на кондитерской фабрике. Конфеты едут к упаковщику. Если лента едет быстрее, чем он упаковывает, конфеты копятся. Сначала на столе, потом на полу. Есть три выхода:
- Копить — поставить ящик побольше. Работает, пока ящик не кончится.
- Выбрасывать — лишние конфеты в мусор. Иногда это честный вариант.
- Тормозить ленту — упаковщик дёргает рычаг, и лента едет медленнее.
Backpressure — это третий выход. Медленный участок управляет скоростью быстрого. Не наоборот.
Как это работает в коде
Данные в программе часто идут потоком: файл читается кусками, ответ сервера приходит частями, строки из базы — пачками. У каждого потока есть источник и получатель. И у получателя есть буфер — тот самый ящик для конфет.
В Node.js этот ящик по умолчанию на 64 КБ (с Node 22, раньше было 16 КБ). Когда ты пишешь в поток, метод write() возвращает ответ:
true— «давай ещё, место есть»;false— «буфер полон, жди событияdrain».
Вот что важно: false — это не ошибка. Данные не теряются. Node послушно принимает и следующий кусок, и следующий. Он просто кладёт их в память. Никто тебя не остановит — кроме самой памяти.
// так течёт: write() говорит «стоп», а цикл не слышит
for (const row of rows) {
out.write(row)
}
// так правильно: на false ждём, пока буфер освободится
import { once } from "node:events"
for (const row of rows) {
if (!out.write(row)) await once(out, "drain")
}
А ещё проще — не писать этот цикл вообще. pipeline() из node:stream/promises соединяет источник и получатель и сам ставит источник на паузу, когда получатель не успевает:
import { pipeline } from "node:stream/promises"
import { createReadStream, createWriteStream } from "node:fs"
await pipeline(createReadStream("big.csv"), createWriteStream("copy.csv"))
Пятигигабайтный файл так копируется с почти ровной памятью. Источник читает ровно столько, сколько получатель готов принять.
Почему это важно тебе
Без backpressure программа ведёт себя предательски. На маленьких данных всё летает. Ты проверил на файле в 10 МБ — отлично. А в проде пришёл файл на 3 ГБ, и процесс упал с JavaScript heap out of memory. Или сервер просто начал тормозить, потому что память кончилась и система полезла в своп.
Это похоже на утечку памяти, но причина другая. Утечка — это память, которую забыли отпустить. Здесь память отпустят. Просто позже, чем ты её набираешь.
И это частый узор в коде от ИИ. Модель пишет «прочитай всё, обработай всё, запиши всё» — потому что на коротком примере это самый понятный код. Попроси прямо: «обрабатывай файл потоком, через pipeline, с учётом backpressure». Одна фраза — и код меняется.
Где ты встретишь backpressure
- Большие файлы. Импорт CSV, видео, логи. Читай потоком, а не целиком.
- База данных. Миллион строк курсором, а запись в другую базу медленнее. Без паузы — гора строк в памяти.
- WebSocket. У сокета в браузере есть
bufferedAmount— сколько байт ещё не ушло. Если клиент на плохом интернете, а ты шлёшь кадры игры 60 раз в секунду, это число растёт. Проверяй его и пропускай кадры. - Запросы к API. Тут backpressure приходит снаружи: сервер отвечает 429 «слишком много». Это то же «притормози», только по сети. Помогает семафор — он не даст запустить больше N запросов сразу.
Чем backpressure отличается от очереди?
Очередь — это и есть «ящик побольше»: она копит. Backpressure решает, что делать, когда ящик полон. Хорошая очередь тоже имеет предел и в какой-то момент говорит отправителю «нет».
А если просто увеличить буфер?
Ты отодвинешь падение, но не уберёшь его. Если источник стабильно быстрее получателя, любой буфер когда-то заполнится. Буфер сглаживает короткие всплески, а не постоянную разницу в скорости.
Есть ли backpressure в браузерном fetch?
Да. Тело ответа в fetch — это ReadableStream. Пока ты читаешь его через getReader() кусками, браузер не тянет данные быстрее, чем ты их забираешь. Поэтому большие ответы лучше читать потоком, а не через await res.text().
Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.


