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:
The fix requires two layers of backpressure working together:
- TCP flow control — the kernel tells the sender to slow down when the receive buffer is full
- 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.
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.
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 writingfalse— buffer has reached HWM, you should stop writing
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.
readable.pause() and readable.resume()
For explicit backpressure control on a readable stream:
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.
With .pipe(), backpressure is handled automatically:
- If
dbWriterreturnsfalsefromwrite(),parserpauses - If
parseris paused,reqpauses - If
reqis 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:
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.
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:
socket.setTimeout(): Eliminating Zombie Connections
Connections that are established but never send data consume file descriptors indefinitely without a timeout:
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:
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:
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:
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
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
| Concept | Key Takeaway |
|---|---|
| TCP flow control | Zero window = sender pauses. Kernel-level backpressure built into TCP. |
highWaterMark | Buffer threshold. When exceeded, write() returns false. |
write() return value | false means pause upstream. true means continue. |
drain event | Buffer 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.writableLength | Current bytes/objects buffered. Monitor this for capacity planning. |
server.maxConnections | Hard cap on simultaneous connections. Refuse rather than hang. |
socket.setTimeout() | Kill zombie connections. Essential for preventing fd exhaustion. |
| HTTP/2 multiplexing | Fewer TCP connections, per-stream backpressure, better fd utilization. |
| Token bucket rate limit | Enforce 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 →
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 & RegisterDiscussion
0Join the discussion