12.01.2026

Node.js’te Backpressure: Stream ve HTTP İsteklerinde Taşmayı Önlemek

Node.js’te backpressure mantığını Stream’ler ve HTTP yanıtlarında doğru akış kontrolüyle uygulayın.

Neden backpressure?

Node.js hızlı I/O yapar; ama “hızlı üretmek”, “hızlı tüketmek” demek değildir. Bir yerden veri okurken (dosya, DB cursor, upstream HTTP) diğer tarafta yavaş bir tüketici varsa bellek şişer, gecikme artar ve sonunda süreç OOM ile düşebilir.

Backpressure, üreticinin hızını tüketiciye göre ayarlama mekanizmasıdır. Node.js bunu özellikle Stream API’sinde doğal olarak sunar.


1) Stream’lerde temel kural: pipe() ve pipeline() kullan

Elle data event’iyle okuyup res.write() yapmak genellikle backpressure’ı yanlış yönetmeye götürür. Bunun yerine pipeline() kullanın: hata yönetimi + akış kontrolü tek yerde.

import http from 'node:http';
import fs from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { createGzip } from 'node:zlib';

http.createServer(async (req, res) => {
  if (req.url === '/download') {
    res.setHeader('Content-Type', 'application/octet-stream');
    res.setHeader('Content-Encoding', 'gzip');

    try {
      await pipeline(
        fs.createReadStream('./big.log'),
        createGzip(),
        res
      );
    } catch (err) {
      // pipeline hata olursa stream’leri doğru kapatır
      res.statusCode = 500;
      res.end('Stream error');
    }
    return;
  }

  res.end('ok');
}).listen(3000);

Ne kazandık?

  • res yavaşlarsa pipe/pipeline otomatik olarak upstream’i yavaşlatır.
  • Hatalar düzgün “propagate” olur.

2) res.write() dönüş değerini ciddiye alın

Bazı durumlarda pipe() kullanamazsınız (ör. chunk’ları dönüştürüp yazmanız gerekiyor). O zaman backpressure manuel yönetilir:

  • res.write(chunk) false dönerse buffer dolmuştur.
  • drain gelene kadar üretimi durdurun.
import http from 'node:http';

http.createServer(async (req, res) => {
  if (req.url !== '/numbers') return res.end('ok');

  res.setHeader('Content-Type', 'text/plain; charset=utf-8');

  for (let i = 0; i < 10_000_000; i++) {
    const ok = res.write(i + '\n');
    if (!ok) {
      // tüketici yetişemiyor, bekle
      await new Promise(resolve => res.once('drain', resolve));
    }
  }

  res.end('done');
}).listen(3000);

Bu küçük kontrol, “yük altında bellek artıyor” tarzı gizli problemleri önler.


3) Readable tarafında da sinyal var: highWaterMark

Stream’ler dahili buffer boyutuna göre çalışır. Büyük chunk’lar veya yoğun trafik varsa varsayılan highWaterMark her senaryoya uygun olmayabilir.

  • Daha küçük highWaterMark: bellek daha kontrollü, ama daha fazla sys-call/overhead.
  • Daha büyük highWaterMark: throughput artabilir, bellek büyür.

Örnek:

fs.createReadStream('./big.bin', { highWaterMark: 64 * 1024 }); // 64KB

Burada amaç “en büyük değer” değil, en stabil değeri bulmak.


4) Async iterator ile akış: modern ve güvenli

Node stream’leri for await...of ile tüketilebilir. Bu yaklaşım, tüketimi sıraya soktuğu için doğal olarak backpressure’a daha yatkındır.

import fs from 'node:fs';
import http from 'node:http';

http.createServer(async (req, res) => {
  if (req.url !== '/tail') return res.end('ok');

  const stream = fs.createReadStream('./big.log');
  for await (const chunk of stream) {
    if (!res.write(chunk)) {
      await new Promise(r => res.once('drain', r));
    }
  }
  res.end();
}).listen(3000);


Pratik kontrol listesi

  • Büyük veri taşıyorsanız: Buffer’ı topluca biriktirmeyin, streamleyin.
  • pipe()/pipeline() öncelikli tercih.
  • Manuel yazıyorsanız: write() sonucu + drain şart.
  • highWaterMark’ı ölçerek ayarlayın (tahminle değil).

Backpressure doğru kurgulandığında Node.js uygulamanız “hızlı” olmaktan öte, yük altında da öngörülebilir ve dayanıklı olur.