Module A-5·23 min read

Implementing backpressure when upstream ingestion velocity outpaces downstream write capacity — socket floods, TCP buffers, and the drain event contract.

Module 4 — The HTTP/TCP Subsystem & Ingestion Backpressure

What this module covers: Backpressure is the mechanism that prevents a fast producer from overwhelming a slow consumer. In a blockchain indexer, the producer is the network delivering transaction events at 50K/sec. The consumer is your PostgreSQL database accepting 5K writes/sec. Without backpressure, your process accumulates an unbounded in-memory queue and eventually OOMs. This module covers how TCP flow control works at the kernel level, how Node.js stream backpressure mirrors it at the application level, and the precise implementation of backpressure for high-throughput ingestion pipelines.


The Problem: Producers Outpacing Consumers

A blockchain indexer at peak load has an inherent mismatch:

  • Producer: blockchain full node pushing 50,000 transaction events/second over TCP
  • Consumer: PostgreSQL database accepting 8,000–12,000 writes/second

Without backpressure, the process does this:

javascript

The fix requires two layers of backpressure working together:

  1. TCP flow control — the kernel tells the sender to slow down when the receive buffer is full
  2. Stream backpressure — Node.js pauses the socket read when the downstream consumer is busy

Understanding how these two layers interact is the key to building a pipeline that never OOMs under load.


TCP Flow Control: The Kernel's Backpressure

TCP has backpressure built in via the receive window mechanism.

Every TCP ACK includes a window field — the number of bytes the receiver is willing to accept. When the kernel's receive buffer fills up, it advertises a smaller window to the sender. When the buffer is full, it advertises zero window — the sender must stop completely.

text

The critical insight: TCP flow control automatically propagates backpressure upstream. When Node.js stops reading from the socket (because downstream is slow), the kernel receive buffer fills, the window shrinks to zero, and the blockchain full node is forced to stop sending. The backpressure is communicated all the way back to the data source — no data is dropped, it is simply slowed.


Node.js Streams: Application-Level Backpressure

Node.js streams implement the same backpressure mechanism at the JavaScript level, mirroring TCP flow control.

The highWaterMark (HWM)

Every writable stream has a highWaterMark — the maximum number of bytes (or objects) it is willing to buffer before signalling that it is full.

javascript

The write() Return Value: The Backpressure Signal

When you write to a writable stream, it returns a boolean:

  • true — buffer is below HWM, safe to continue writing
  • false — buffer has reached HWM, you should stop writing
javascript

The drain Event: The Resume Signal

When the stream's internal buffer empties below HWM after being full, it emits drain. This is your signal to resume writing.

javascript

readable.pause() and readable.resume()

For explicit backpressure control on a readable stream:

javascript

The Danger of Unpaused Readable Streams

A net.Socket is a Readable stream. In Node.js, a readable stream that is not being consumed operates in two modes:

Flowing mode (default when a data event listener is attached): data is emitted as fast as it arrives. If you can't process it, it accumulates in memory.

Paused mode (after socket.pause()): data accumulates in the kernel receive buffer. No JavaScript memory consumption. TCP flow control does its job.

The correct pattern for a high-throughput ingestion socket: monitor write queue depth and pause/resume the socket accordingly.


HTTP Ingestion: IncomingMessage as a Stream

http.IncomingMessage (the req object in an HTTP server) is a Readable stream backed by the underlying TCP socket. The same backpressure rules apply.

javascript

With .pipe(), backpressure is handled automatically:

  • If dbWriter returns false from write(), parser pauses
  • If parser is paused, req pauses
  • If req is paused, the underlying TCP socket is paused
  • TCP socket paused → kernel receive buffer fills → sender slows

writable.writableLength and writable.writableHighWaterMark

Real-time monitoring of stream buffer state:

javascript

HTTP/2: Multiplexed Streams for Persistent Ingestion Connections

For blockchain indexers that maintain persistent connections to full nodes, HTTP/2 provides stream multiplexing over a single TCP connection.

javascript

Benefit for blockchain indexers: instead of 1,000 separate TCP connections from 1,000 full nodes, use 10 HTTP/2 connections each multiplexing 100 streams. Fewer file descriptors, fewer TCP handshakes, same data throughput. Each stream has independent backpressure.


Handling Socket Floods: server.maxConnections

When a blockchain network goes through a major upgrade, every node may attempt to reconnect simultaneously (similar to the thundering herd from Module 3). server.maxConnections provides a hard cap:

javascript

socket.setTimeout(): Eliminating Zombie Connections

Connections that are established but never send data consume file descriptors indefinitely without a timeout:

javascript

Rate Limiting at the TCP Layer

Before requests reach your application routing logic, you can enforce rate limits at the connection level using token bucket algorithms:

javascript

The Production Incident: OOM from Missing Backpressure During Airdrop

Context: A UPI payment processing gateway ingesting payment events from multiple upstream aggregators. Normal throughput: 3,000 events/second. During a major e-commerce sale, 35,000 events/second arrived simultaneously.

The broken pipeline:

javascript

At 35,000 events/sec with 8,000 db writes/sec capacity:

  • Second 1: 27,000 writes queued in memory
  • Second 5: 135,000 writes queued → ~270MB
  • Second 12: ~380,000 writes queued → ~760MB
  • Second 14: Process OOM-killed by the OS

The Kubernetes pod restarted. On restart, the TCP connections reconnected, and the same flood resumed. The pod crashed again in 14 seconds. Kubernetes kept restarting it. This cycle continued for 8 minutes until the upstream aggregators hit their own connection retry limits.

The fix — stream pipeline with backpressure:

javascript

Result: Under 35,000 events/sec, the pipeline automatically throttled to 8,000/sec — the database's write capacity. Memory stabilized at ~12MB (the 500-object buffer × ~24KB per object). No OOM. The upstream aggregator experienced slightly higher latency during the peak, but no data was lost.


Backpressure Monitoring

javascript

Tracking nodejs_stream_backpressure_total over time tells you exactly how often your pipeline is capacity-constrained — invaluable for right-sizing your database write pool and scaling decisions.


Summary

ConceptKey Takeaway
TCP flow controlZero window = sender pauses. Kernel-level backpressure built into TCP.
highWaterMarkBuffer threshold. When exceeded, write() returns false.
write() return valuefalse means pause upstream. true means continue.
drain eventBuffer drained below HWM. Safe to resume writing.
readable.pause()Stops consuming from kernel receive buffer. TCP backpressure propagates upstream.
.pipe()Handles pause/resume automatically. Correct default for most pipelines.
writable.writableLengthCurrent bytes/objects buffered. Monitor this for capacity planning.
server.maxConnectionsHard cap on simultaneous connections. Refuse rather than hang.
socket.setTimeout()Kill zombie connections. Essential for preventing fd exhaustion.
HTTP/2 multiplexingFewer TCP connections, per-stream backpressure, better fd utilization.
Token bucket rate limitEnforce per-IP limits before requests reach application logic.

Backpressure prevents memory exhaustion from the outside in. Module 5 goes inside — how to process gigabytes of data without touching the V8 heap at all, using off-heap Buffers, Transform streams, and streaming pipelines for blockchain transaction log processing.

Next: Module 5 — Native Streams & Off-Heap Buffer Storage →


Knowledge Check

What kernel-level mechanism causes an upstream sender to slow down when a Node.js application calls socket.pause() on a fast readable socket?


Why is piping streams using req.pipe(parser).pipe(dbWriter) superior to concatenating the payload via req.on('data', ...) and processing it synchronously?


What happens to incoming TCP data immediately after socket.pause() is executed in a Node.js application?

Test your knowledge with more question sets

Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.