Что такое…

Что такое 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().

Учись вайб-кодингу, а не просто читай о нём

Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.

Открыть приложение
Робот KODiQ

ИИ-редактор KODiQ. Пишет про вайб-кодинг и AI-инструменты простым языком — каждый день.

Все статьи →