NODE.JS STREAMS VE BACKPRESSURE NEDİR? BÜYÜK VERİYİ GÜVENLİ İŞLEMEK
Büyük bir dosyayı okurken veya yoğun bir API yanıtını işlerken “her şeyi belleğe alayım, sonra işlerim” yaklaşımı bir noktada duvara çarpar. Node.js’in stream mimarisi tam da bu yüzden vardır: veriyi parça parça akıtarak hem bellek kullanımını kontrol altında tutar, hem de I/O ile CPU işlerini dengeler. Ancak stream’leri gerçekten güvenli yapan ikinci bir kavram var: backpressure.
Backpressure, en basit haliyle “üreten taraf hızlı, tüketen taraf yavaşsa ne olacak?” sorusunun cevabıdır. Akışın bir yerinde yığılma başladığında sistemin kendini koruyacak şekilde yavaşlaması gerekir; aksi halde buffer büyür, bellek şişer, gecikmeler artar ve sonunda süreç çökebilir. Node.js stream ekosistemi bu yavaşlamayı doğal akışın içine yerleştirir; doğru kullanırsanız büyük veri işleme senaryolarında beklenmedik bellek patlamalarını büyük ölçüde engellersiniz.
Bu makalede stream türlerini, backpressure’ın nasıl oluştuğunu, Node.js’in bunu hangi mekanizmalarla yönettiğini ve gerçekçi kod örnekleriyle nasıl kontrol edebileceğinizi adım adım ele alacağız. Amaç “sadece çalışsın” değil; tahmin edilebilir, ölçeklenebilir ve dayanıklı bir akış tasarlamak.
Stream yaklaşımı: Neden “akış” büyük veride altın standart?
Stream’ler veriyi küçük parçalara bölerek (chunk) işler. Bu sayede bellek tüketimi toplam veri boyutuna değil, aynı anda elde tutulan chunk’ların büyüklüğüne bağlı olur. Örneğin 10 GB’lık bir log dosyasını tek seferde okumanız mümkün değildir; ama stream ile satır satır, blok blok ilerleyebilirsiniz.
Stream yaklaşımı ayrıca gecikmeyi düşürür: ilk chunk gelir gelmez işlemeye başlayabilirsiniz. Bu, kullanıcıya daha hızlı yanıt vermek veya pipeline’ın bir sonraki adımını erken beslemek açısından önemlidir. Üstelik Node.js’in event loop mimarisi ve I/O odaklı doğası, stream kullanımını doğal bir tercih haline getirir.
Chunk, buffer ve “akış boru hattı” fikri
Stream dünyasında veri “chunk” olarak akar. Bu chunk çoğu zaman Buffer’dır (binary), bazen string olabilir. Okuyan taraf (Readable) chunk üretir, yazan taraf (Writable) chunk tüketir, aradaki dönüştürücüler (Transform) veriyi değiştirerek iletir. Bu yapı bir boru hattı (pipeline) gibi düşünülebilir: her adım bir sorumluluk alır, sistem bütünü daha yönetilebilir hale gelir.
Backpressure nedir ve hangi problemden doğar?
Backpressure, tüketim kapasitesi üretim kapasitesini karşılamadığında oluşan basınçtır. Örneğin diskten okuma çok hızlı olabilir ama ağ üzerinden gönderme yavaşsa, Writable tarafın iç buffer’ı dolmaya başlar. Üretim tarafı durmazsa buffer büyür; büyüdükçe bellek artar, GC baskısı yükselir, latency zıplar.
Node.js stream’lerinde backpressure, özellikle Writable’ın write() çağrısına verdiği geri bildirimle yönetilir. write() false döndürdüğünde bu, “şu an buffer dolu, beni bir süre rahat bırak” demektir. Bu noktada üretimi yavaşlatmak, akış kontrolü için kritiktir.
Backpressure’ın işareti: write() neden false döner?
Writable stream’lerde iç buffer bir eşiği aşınca write() false döndürür. Bu eşik genellikle highWaterMark ile ilişkilidir. Buradaki değer bir “limit” değil; akışın dengede kalması için kullanılan bir eşik gibi düşünülmelidir. false döndüğünde doğru davranış, yeni chunk üretmeyi durdurmak ve drain event’ini beklemektir.
highWaterMark neyi belirler, neyi belirlemez?
highWaterMark çoğu geliştirici tarafından yanlış anlaşılır. Bu değer “buffer tam kapasitesi” değil, “geri basınç sinyali verme eşiği”dir. Çok düşük ayarlarsanız aşırı sık dur-kalk yaparsınız; çok yüksek ayarlarsanız bellek tüketimi artar. Doğru değer; veri türüne, ortalama chunk boyutuna, hedef I/O hızına ve sistem kaynaklarına göre değişir.
Node.js stream türleri ve backpressure ile ilişkileri
Node.js tarafında dört temel stream türü vardır: Readable (okur), Writable (yazar), Duplex (hem okur hem yazar) ve Transform (dönüştürür). Backpressure kavramı özellikle Readable → Writable akışında hissedilir; çünkü üretim ve tüketim hızları farklı olduğunda “kim kimi bekleyecek?” sorusu ortaya çıkar.
Readable: Akışı üretirken durabilmek
Readable stream’ler, tüketicinin hızına uyum sağlayabilmelidir. Modern Node.js’te bunun en temiz yollarından biri async iterator kullanmaktır. Bu yaklaşım, okuma hızını “await” ile doğal olarak tüketiciye bağlar ve backpressure’ı daha anlaşılır bir kontrol akışıyla yönetmenizi sağlar.
Writable: drain event’i ile ritmi belirlemek
Writable tarafın drain event’i, buffer’ın yeniden yazılabilir hale geldiğini bildirir. Bu event’i doğru kullanmak, özellikle manuel yazma döngülerinde kritik olur. Aksi halde “yazmaya devam et” diye bastırdığınızda sistem gecikmeye ve bellek şişmesine sürüklenir.
pipe ve pipeline: Backpressure çoğu zaman “bedava” gelir
Node.js’te readable.pipe(writable) kullanıldığında backpressure çoğu senaryoda otomatik yönetilir. Writable taraf yavaşladığında pipe mekanizması Readable’ı duraklatır ve buffer dengesi sağlanır. Bu yüzden mümkün olduğunda pipe veya stream/promises içindeki pipeline() tercih edilir.
Neden pipeline? Hata ve kapanış semantiği
Basit pipe zincirlerinde hata yakalama ve kaynak kapatma bazen dağınık hale gelebilir. pipeline() hem error propagation’ı daha net yapar hem de stream’leri doğru şekilde sonlandırır. Bu da büyük veri işleme hattında “yarım kalan” dosya tanıtıcıları veya kapanmayan socket’ler gibi sorunları azaltır.
Örnek 1: Dosyadan ağ akışına güvenli aktarım (pipeline)
Aşağıdaki örnekte diskten okunan veriyi bir HTTP isteğine gövde olarak gönderdiğimizi düşünün. Burada amaç: dosya boyutu ne olursa olsun belleği şişirmeden, yazma tarafının hızına uyarak ilerlemek. Örnek kod; fikir vermek içindir ve gerçek sistemde zaman aşımı, yeniden deneme ve gözlemlenebilirlik katmanları eklenmelidir.
const fs = require('node:fs');
const http = require('node:http');
const { pipeline } = require('node:stream/promises');
async function uploadFile(filePath, host, path) {
const req = http.request({
method: 'POST',
host,
path,
headers: {
'Content-Type': 'application/octet-stream'
}
});
req.on('response', (res) => {
// response tüketimi de önemlidir; aksi halde socket kapanışı gecikebilir
res.resume();
});
const fileStream = fs.createReadStream(filePath, {
highWaterMark: 1024 * 256 // 256KB, örnek değer
});
await pipeline(fileStream, req);
}
uploadFile('./big.bin', 'example.com', '/upload')
.then(() => console.log('done'))
.catch((err) => console.error('failed', err));Bu akışta backpressure, req (Writable) tarafının iç buffer’ı üzerinden yönetilir. Ağ tıkanırsa req.write() basınç sinyali üretir ve pipeline Readable’ı duraklatır. Yani “dosya çok hızlı okunuyor” paniği yerine, sistemin kendi hızını ayarlamasına izin verirsiniz.

Örnek 2: Manuel yazma döngüsünde drain ile backpressure yönetimi
Bazen pipe kullanamazsınız: örneğin chunk’ları paketleyip özel bir protokolle göndermeniz gerekir ya da her chunk üzerinde ek bir iş yaparsınız. Bu durumda yazma tarafının false döndürmesini ciddiye almak şarttır. Aşağıdaki örnek, Readable’dan gelen chunk’ları manuel biçimde Writable’a yazar ve drain ile ritmi kontrol eder.
const fs = require('node:fs');
const { once } = require('node:events');
async function copyWithBackpressure(src, dest) {
const readable = fs.createReadStream(src);
const writable = fs.createWriteStream(dest);
readable.on('error', (e) => writable.destroy(e));
writable.on('error', (e) => readable.destroy(e));
for await (const chunk of readable) {
const ok = writable.write(chunk);
if (!ok) {
await once(writable, 'drain');
}
}
writable.end();
await once(writable, 'finish');
}
copyWithBackpressure('./in.log', './out.log')
.then(() => console.log('copied'))
.catch((e) => console.error(e));Buradaki kritik nokta şudur: Writable yavaşladığında okumayı bekletiyorsunuz. Bu, üretim hızını tüketim kapasitesine bağlar. Async iterator ile okuma yaptığınız için, kontrol akışı daha okunur ve hata senaryoları daha yönetilebilir hale gelir.

Transform stream ile “işleme maliyeti”ni dengelemek
Birçok gerçek senaryoda sadece kopyalama yapmazsınız; sıkıştırma, şifreleme, satır ayrıştırma, JSON dönüşümü gibi işler eklersiniz. İşte bu noktada Transform stream’ler hem modülerlik sağlar hem de backpressure zincirini korur. Transform’ün yavaşlaması, üst akışın da yavaşlamasına yol açar; böylece CPU yoğun bir adım bütün sistemi bilinçsizce şişirmez.
Batching ve veri şekillendirme
Örneğin satır bazlı bir log akışında her satırı tek tek göndermek yerine küçük paketler halinde (batch) göndermek ağ verimliliğini artırabilir. Ancak batching yaparken buffer’ı büyütüp yeni bir bellek riski yaratmamak gerekir. Transform içinde biriktirme mantığını sınırlı tutmak ve gerektiğinde flush etmek doğru yaklaşımdır.
Object mode kullanırken dikkat: highWaterMark anlamı değişir
Object mode’da chunk’lar byte değil, “nesne sayısı” olarak değerlendirilir. Yani highWaterMark bu sefer “kaç nesne tamponlanacak?” sorusuna cevap verir. Nesneler büyükse veya derin yapılar taşıyorsa, düşük bir eşik daha iyi olabilir. Bu detay atlanırsa, bellek tüketimi beklenenden hızlı artabilir.
Hata yönetimi, kaynak temizliği ve iptal senaryoları
Büyük veri işleme hattında hata kaçınılmazdır: disk okuma hatası, bağlantı kopması, izin problemi, parse hatası… Burada önemli olan, hata olduğunda stream’leri doğru sırayla kapatmak ve kaynakları serbest bırakmaktır. Pipe zincirlerinde pipeline() bu konuda daha güvenlidir; manuel kurgularda ise error event’lerini dikkatle ele almak gerekir.
AbortController ile iptal edilebilir akışlar
Uzun süren bir aktarımı kullanıcı iptal edebilir ya da bir zaman aşımı devreye girebilir. Modern Node.js sürümlerinde AbortController ile pek çok API iptal desteği sunar. Stream tarafında da iptal yaklaşımını tasarlamak, kaynak sızıntılarını engeller ve sistemi daha dayanıklı kılar.
Performans ve gözlemlenebilirlik: “Çalışıyor” yetmez
Backpressure doğru yönetilse bile, darboğazın nerede olduğunu anlamak için ölçüm gerekir. Log’larınızda aktarım hızı, ortalama chunk boyutu, gecikme, hata oranı gibi metrikleri takip etmek önemlidir. Ayrıca yük testi yapılmadan, sadece geliştirme ortamındaki davranışa bakarak karar vermek risklidir; üretimde disk, ağ, CPU ve GC dinamikleri farklılaşır.
Pratik kontrol listesi
- Pipe/pipeline kullanabiliyorsanız önce onları tercih edin.
- Manuel yazma yapıyorsanız
write()sonucunu izleyin;falseisedrainbekleyin. highWaterMarkdeğerini körlemesine büyütmeyin; veri türü ve hedef I/O hızına göre ayarlayın.- Transform adımlarında biriktirme (batch) mantığını sınırlı tutun.
- Hata ve iptal senaryolarında stream’lerin kapanışını test edin.

Node.js stream ve backpressure’ı birlikte öğrenmek için yol haritası
Stream’ler, Node.js ekosisteminde dosya işleme, ağ aktarımı, ETL pipeline’ları, medya işleme ve kuyruk tüketimi gibi pek çok alanda temel bir yapı taşıdır. Backpressure ise bu yapının “sigortası”dır: sistemin kapasitesini aşmadan, kontrollü biçimde ilerlemesini sağlar. Bu iki kavramı birlikte kavradığınızda, daha az bellek tüketen ve daha stabil çalışan servisler tasarlamak kolaylaşır.
Bu konuyu daha derinlemesine, gerçek projelerde sık görülen senaryolarla pekiştirmek isterseniz Node.js Eğitimi sayfasına göz atabilirsiniz. Orada stream tabanlı mimariler, performans tuzakları ve üretim ortamı pratikleri gibi başlıklara daha kapsamlı yaklaşabilirsiniz.
Özetle: “Büyük veriyi güvenli işlemek” çoğu zaman daha güçlü donanım değil, doğru akış kontrolüdür. Stream’leri doğru kurun, backpressure sinyallerini dinleyin ve sistemi kapasitesine göre konuşturun; gerisi çok daha öngörülebilir hale gelir.


