惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 聂微东
GbyAI
GbyAI
S
SegmentFault 最新的问题
H
Hackread – Cybersecurity News, Data Breaches, AI and More
V
Visual Studio Blog
WordPress大学
WordPress大学
Hugging Face - Blog
Hugging Face - Blog
B
Blog
宝玉的分享
宝玉的分享
Last Week in AI
Last Week in AI
雷峰网
雷峰网
爱范儿
爱范儿
Vercel News
Vercel News
人人都是产品经理
人人都是产品经理
U
Unit 42
Microsoft Azure Blog
Microsoft Azure Blog
Microsoft Security Blog
Microsoft Security Blog
Jina AI
Jina AI
P
Proofpoint News Feed
A
About on SuperTechFans
I
InfoQ
F
Fortinet All Blogs
L
LangChain Blog
T
Tailwind CSS Blog

The Practical Developer

The Libuv Thread Pool Trap: Why Node.js Async APIs Stall Under Load Postgres Covering Indexes with INCLUDE: Eliminate Heap Fetches on Read-Heavy Workloads Postgres DISTINCT ON: The Fastest Way to Get the Latest Row Per Group Postgres Transaction Isolation: The Anomalies Your App Actually Faces in Production Linux TCP Tuning for Node.js Microservices: The Kernel Settings That Stop Silent Connection Drops Under Load Postgres HOT Updates and Fillfactor: Why Not All Writes Are Created Equal Database Connection Pool Leaks: Finding the Promise That Never Returns Its Seat Linux OOM Killer in Production: Why Your Node.js Containers Die Without a Stack Trace Postgres Materialized Views: Refresh Strategies That Do Not Lock Your Dashboards API Dependency Health Checks: Why /health Is Not Enough Authorization with Zanzibar Tuples: How Google Manages Permissions and How To Build the Same Check in Node.js Postgres Advisory Locks: The 20-Character Primitive That Replaces Redis for Coordination Dead Letter Queues: The Message Queue Pattern That Saves You at 2 a.m. File Descriptor Exhaustion: The Kernel Limit That Silently Drops Node.js Connections Graceful Degradation: The Pattern That Turns Total Outages into Partial Success PostgreSQL Full-Text Search: Dropping Elasticsearch for 90% of Use Cases S3 Presigned Multipart Uploads: Stop Your API Server from Being a File Upload Bottleneck MessagePack vs JSON: The Binary Serialization Switch That Cut Our Internal RPC Overhead by 40% DNS Caching in Node.js: The Silent Cause of Production Latency Spikes Reliable Cron Jobs: The Pattern That Stops Double Runs, Missed Executions, And The 2 AM Page GraphQL Query Complexity: Stop the OOM Query Before It Reaches Your Resolver Node.js Event Loop Lag: The Hidden Metric Behind Random Latency Spikes API Request Validation with Zod: The Schema That Catches Bad Input Before It Corrupts Your Database Load Shedding in Node.js: How to Reject Traffic Before You Drown Request Hedging: Cut Tail Latency In Half Without Overprovisioning Git Bisect: The Automated Binary Search That Finds Breaking Commits in Minutes Node.js Garbage Collection Tuning: Stop Letting V8 Pause Your Event Loop Node.js Server Timeouts: The Settings That Stop Slow Clients from Holding Sockets Hostage Postgres BRIN Indexes: The Time-Series Secret That Shrinks Indexes by 99% Event Sourcing with PostgreSQL: The Pragmatic 80% Solution
Backpressure In Node.js: The Fix For Slow-Motion Queue Me...
The Practica · 2026-05-05 · via The Practical Developer

Your worker is “healthy.” CPU is fine. No 500s. No restart loops.

But queue lag keeps climbing. Memory grows all day. Retries spike. By evening, one dependent API slows down and your whole pipeline falls over.

That is a backpressure failure.

Most teams treat this as a scaling problem (“add more pods”). Usually it is a flow-control problem: producers can create work faster than consumers can safely process it, and nothing in the system says “slow down.”

This is the practical fix in Node.js: bounded concurrency, explicit in-flight limits, and stream-aware processing that applies pressure before memory bloats.

What backpressure actually means

Backpressure is a feedback signal from a slower stage to a faster stage.

  • Producer: creates messages/jobs/chunks
  • Consumer: processes them
  • Backpressure: producer pauses or reduces rate when consumer is saturated

Without it, your app buffers unbounded data in memory (or in queues) and dies slowly.

With it, throughput stays close to the real downstream capacity.

The anti-pattern that causes incidents

A common worker loop looks safe but is not:

for (const job of jobs) {
  processJob(job); // fire-and-forget
}

Or slightly “better”:

await Promise.all(jobs.map(processJob));

Both can overload downstream services and your own process memory.

Symptoms you can observe in production:

  • Queue age rising faster than dequeue rate
  • Heap usage ratcheting upward, never returning to baseline
  • Spiky p95/p99 from dependency throttling
  • Retry storms (same payload processed many times)
  • High GC time despite normal CPU

A safe baseline: bounded concurrency worker

Start with a fixed in-flight limit.

import pLimit from 'p-limit';

const CONCURRENCY = 20;
const limit = pLimit(CONCURRENCY);

export async function processBatch(messages) {
  const tasks = messages.map((msg) =>
    limit(async () => {
      await handleMessage(msg);
    })
  );

  const results = await Promise.allSettled(tasks);

  // Ack only succeeded messages; Nack/requeue failed ones explicitly.
  return results;
}

Why this works:

  • Max 20 jobs active at once
  • New jobs wait in a tiny scheduler queue, not your custom arrays
  • Downstream systems see stable pressure instead of burst floods

This single change removes a lot of “random” incidents.

Make it adaptive (when downstream gets slower)

Fixed concurrency is good. Adaptive concurrency is better under variable latency.

A simple rule:

  • Track rolling p95 latency for handleMessage
  • If p95 exceeds threshold for N windows, reduce concurrency (e.g., -20%)
  • If p95 stays healthy for M windows, increase slowly (+1)

Keep guardrails:

  • min concurrency (e.g., 4)
  • max concurrency (e.g., 64)
  • cooldown period between adjustments

Do not chase every second of noise; adjust every 15–30s window.

Stream pipelines: respect write() return values

If you process large payloads/files/events via streams, Node already gives you backpressure signals.

The key rule: when writable.write(chunk) returns false, stop writing until drain.

import { once } from 'node:events';

async function pump(readable, writable) {
  for await (const chunk of readable) {
    if (!writable.write(chunk)) {
      await once(writable, 'drain');
    }
  }
  writable.end();
}

Better: use pipeline() so error handling and teardown are correct.

import { pipeline } from 'node:stream/promises';

await pipeline(sourceStream, transformStream, sinkStream);

If you ignore this and keep writing, you create hidden in-memory queues and eventually OOM.

Queue consumer settings that matter

Backpressure is not only code. Broker settings must match.

For RabbitMQ-style workers:

  • Set prefetch to your real in-flight limit (or a small multiple)
  • Manual ack only after successful processing
  • Dead-letter poison messages after bounded retries

For Kafka-style consumers:

  • Cap max records per poll
  • Pause partition consumption when internal queue is full
  • Commit offsets only after durable success path

If broker fetch size is huge while app concurrency is tiny, you still buffer too much.

Minimal instrumentation (no fancy platform needed)

You need four graphs per worker:

  • in_flight_jobs (gauge)
  • queue_lag_seconds (gauge/histogram)
  • job_latency_ms p50/p95/p99 (histogram)
  • retry_rate + dlq_rate (counter)

Alert on trends, not single spikes:

  • queue lag increasing for 10+ minutes
  • p95 latency > threshold and in-flight pinned at max
  • retry rate jump + dependency 429/5xx correlation

This tells you whether to lower concurrency, scale horizontally, or fix a dependency.

Incident playbook: when lag is already rising

  1. Freeze pressure: temporarily lower consumer concurrency and prefetch.
  2. Protect dependencies: enforce per-dependency rate limit + jittered retries.
  3. Drain safely: increase replicas only after per-pod limits are correct.
  4. Drop bad traffic: route poison messages to DLQ quickly.
  5. Postmortem metric: identify which stage first exceeded sustainable throughput.

If you only add pods first, you often amplify overload against the same dependency.

Practical defaults you can copy

  • Worker concurrency: min(2 * vCPU, 32) as a starting point
  • Broker prefetch: 1x–2x concurrency
  • Retry policy: exponential backoff + jitter, max 5 attempts
  • Timeout budget: strict per dependency (do not wait forever)
  • Memory budget alarm: page at 70% of container limit, not 95%

Tune from there using real p95 and queue-age trends.

Closing

Backpressure is not an optimization. It is a reliability control.

If your service handles asynchronous work and you cannot answer “where do we apply pressure when downstream slows?”, you have an outage with a timestamp missing.

Add bounded concurrency, respect stream drain signals, and align broker fetch with real processing capacity. Most “mystery queue meltdowns” disappear after that.

A lot of delivery teams that build high-throughput Node.js systems treat this as a non-negotiable baseline. Yojji, for example, often emphasizes these reliability controls in backend and cloud-heavy projects where scaling safely matters more than peak benchmark numbers.