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

推荐订阅源

T
Threatpost
WordPress大学
WordPress大学
Martin Fowler
Martin Fowler
博客园 - 聂微东
L
LangChain Blog
D
Docker
大猫的无限游戏
大猫的无限游戏
人人都是产品经理
人人都是产品经理
D
DataBreaches.Net
aimingoo的专栏
aimingoo的专栏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
腾讯CDC
C
Cisco Blogs
L
LINUX DO - 热门话题
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
有赞技术团队
有赞技术团队
Forbes - Security
Forbes - Security
C
Cybersecurity and Infrastructure Security Agency CISA
博客园 - 三生石上(FineUI控件)
S
Secure Thoughts
N
News and Events Feed by Topic
Latest news
Latest news
PCI Perspectives
PCI Perspectives
TaoSecurity Blog
TaoSecurity Blog
博客园 - 司徒正美
量子位
Stack Overflow Blog
Stack Overflow Blog
Know Your Adversary
Know Your Adversary
SecWiki News
SecWiki News
美团技术团队
C
CERT Recently Published Vulnerability Notes
Attack and Defense Labs
Attack and Defense Labs
AWS News Blog
AWS News Blog
C
Check Point Blog
S
Security Affairs
Vercel News
Vercel News
Google DeepMind News
Google DeepMind News
K
Kaspersky official blog
IT之家
IT之家
A
About on SuperTechFans
Help Net Security
Help Net Security
博客园 - 【当耐特】
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Recorded Future
Recorded Future
月光博客
月光博客
博客园_首页
小众软件
小众软件
H
Help Net Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
爱范儿
爱范儿

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 Node.js Cluster Mode: Scaling the Event Loop Across CPU Cores Postgres Partial Indexes: Stopping Soft Deletes from Ruining Your Query Performance Request Coalescing with the Singleflight Pattern: Stop Drowning Your Database on Every Cache Miss The Bulkhead Pattern: Why One Slow Endpoint Should Not Drown Your Whole Service Node.js AsyncLocalStorage: End-to-End Request Context Without the Propagation Hell Postgres Deadlocks: Logging the Victim, Reproducing the Race, and Fixing the Lock Order Your Node.js HTTP Client Is the Bottleneck: Connection Pool Tuning That Works Optimistic Locking in Postgres: Stop Losing Data to Race Conditions Postgres Read Replicas: Stop Serving Stale Data to Your Users Cursor Pagination: Why Offset Queries Explode at Scale and How to Fix Them Node.js Worker Threads: 60 Lines That Stop a CSV Upload from Timing Out Every Other Request Reliable Webhook Delivery: Architecture for Outbound HTTP You Can Trust Request Timeouts and Deadline Propagation: Stop the Chain of Slowness Advanced Security Practices in Node.js Graceful Shutdown in Node.js: The 40 Lines That Stop 502s During Deploys Finding Node.js Memory Leaks with Heap Snapshots Idempotency Keys in 30 Lines: Stop Your Webhook From Charging Customers Twice Backpressure In Node.js: The Fix For Slow-Motion Queue Meltdowns Retries Done Right: Jitter, Budgets, and the Stampede You Did Not See Coming The Cache Stampede: Why Your "Just Add Redis" Layer Crashes Postgres at 3 a.m. Postgres SKIP LOCKED: An 80-Line Job Queue You Can Run Without Redis Stop Doing Work Nobody Wants: AbortController in Node.js, Done Right The N+1 Query Problem: We Found 23 In One Codebase And Killed Every One I Tried 5 AI Coding Tools for a Month. Here Is What I Actually Use CI/CD From Zero to Production in 30 Minutes With GitHub Actions Node.js vs Bun vs Deno: Which Runtime Should You Pick in 2025? Kubernetes Resource Requests And Limits: The Numbers That Decide If Your Cluster Is Stable The Three Pillars of Observability Are A Myth: What Actually Matters In Production pnpm Vs npm Vs yarn Vs Bun For Monorepos: Which One Earns The Migration In 2024 JSONB Indexing In Postgres: GIN Vs Expression Indexes, And When Each Is The Right Choice A Code Review Checklist That Ends The Same Three Arguments Every Sprint gRPC Vs REST In 2024: When The Switch Pays For Itself React Suspense For Data Fetching: The Pattern That Replaces Half Your Loading State Code The Five-Stage Rollout: How To Ship A Risky Change Without Holding Your Breath GitHub Actions In A Monorepo: Caching, Path Filters, And Secret Boundaries That Actually Work The Blameless Postmortem That Actually Improves Things: A Template And Six Hard-Won Rules Recursive CTEs In Postgres: How To Query A Tree Without N Round Trips Node.js Streams: When They Actually Help, And When They Just Add Complexity Playwright Vs Cypress In 2024: The Honest Comparison Of Which One Earns The Test Time React Server Components: The Mental Model That Makes The "use client" Boundary Obvious Pod Disruption Budgets: The K8s Object That Keeps Your Service Up During Cluster Maintenance Postgres LISTEN/NOTIFY: The Pub/Sub You Already Have And Are Not Using Chaos Engineering Starter Kit: The Five Drills That Don't Need Netflix-Scale Spec-Driven API Development With OpenAPI: How To Stop Drifting From Your Docs Kubernetes Autoscaling Beyond CPU: The Custom-Metric HPA Pattern That Actually Works Postgres Partitioning For Time-Series: The Boring Setup That Saves Your Database Distributed Locks With Redis: An Honest Look At Redlock And When You Don't Need It HTTP/2 vs HTTP/3: What Actually Changes For Your App, And What Doesn't Image Optimization For The Web In 2023: srcset, AVIF, And The Lighthouse Score You Actually Want Kafka vs RabbitMQ: A Decision Tree That Doesn't Hate You UUID vs Bigint Primary Keys In Postgres: The Index Math That Decides For You Flame Graphs: How To Find The Slow Function In 30 Seconds Without Profiling Theatre Postgres Streaming Vs. Logical Replication: Which One Solves Your Actual Problem ESLint Rules That Earn Their Keep: The Twelve I Enable On Every Project Pre-Commit Hooks That Pay For Themselves: Husky, lint-staged, And The Five Rules That Stick Zero-Downtime Database Migrations: The Six-Step Pattern That Rules Them All Circuit Breakers In Node.js: 50 Lines That Stop A Failing Dependency From Taking Down Your Service Postgres VACUUM Is Not Magic: How Your Hot Table Bloats To 80GB And How To Fix It Kubernetes Liveness And Readiness Probes: The Difference That Causes Half Your Outages Rate Limiting In Production: A Token Bucket In 30 Lines Of Redis The Outbox Pattern: How To Stop Losing Events When Postgres And Kafka Disagree Load Testing With k6: The Three Scenarios That Find Real Bugs (Not Synthetic Numbers) Postgres Row-Level Security For Multi-Tenant Apps: The Pattern That Stops You From Leaking Data Rebase vs. Merge: The Team Policy That Ends The Argument Forever OpenTelemetry in Node.js: Distributed Tracing That Actually Helps During an Incident Feature Flags That Pay Rent: The 4 Flag Types And When To Delete Each ETag, Last-Modified, and the Caching Headers Most APIs Get Wrong Connection Pooling Without the Cargo Cult: pgbouncer in 100 Lines of Config JSONB Is Not a Schema: When To Reach For It in Postgres, And When To Stop Bash Strict Mode: The Three Lines That Stop Your Deploy Script From Lying To You
PostgreSQL Connection Timeouts in Node.js: The Four Timers That Prevent Production Outages
The Practica · 2026-06-19 · via The Practical Developer

The production pager went off at 3:14 AM. All API endpoints were returning 502 Bad Gateway. The Node.js processes were still running. The Postgres server was up and accepting connections. But every query was hanging until the load balancer’s 30-second timeout kicked in, which caused a cascading retry storm that kept the connection pool saturated for twelve minutes after the root cause was resolved.

The root cause was not a slow query. It was not an infrastructure failure. It was a DNS resolution hiccup on the database hostname that lasted four seconds. The connection pool had no timeout configured for establishing connections, so every request that tried to grab a fresh connection waited forever on a TCP SYN packet that was never going to complete. By the time the DNS cache refreshed, every request in flight had been retried five times, and the pool was full of orphaned connection attempts that had to be garbage-collected by the OS.

This is the silent failure mode of misconfigured database connection timeouts. It does not throw a clear error. It just queues work behind a connection that is never going to complete. And because it looks like a full system outage from the client’s perspective, every automated retry cycle makes it worse.

Here are the four timers that prevent this scenario, how to set each one with node-postgres, and the failure modes they protect against.

Timer 1: connectionTimeoutMillis

This is the maximum time the client will wait for a TCP connection to the Postgres server to be established. In node-postgres (the pg package), this defaults to 10_000 (10 seconds). That is too long for most production applications.

When a connection attempt times out, the Pool throws an ETIMEDOUT error and the caller gets a clean rejection. Without this timeout, the call stack freezes while the OS waits for a SYN-ACK from a server that may have restarted, changed IP, or become unreachable due to a firewall rule change.

const { Pool } = require('pg');

const pool = new Pool({
  host: 'db.production.internal',
  port: 5432,
  user: 'app',
  database: 'appdb',
  connectionTimeoutMillis: 3000, // 3 seconds, not 10
});

Set this to 3 seconds for internal networks (same VPC, sub-millisecond latency) and 5 seconds for cross-region or VPN connections. A connection that takes longer than 3 seconds inside a data center is not going to succeed if you wait longer. It is going to eat a connection slot and amplify the problem.

The exception is serverless environments like AWS Lambda with RDS Proxy. Cold starts can trigger connection setup times of 5-8 seconds, so a 10-second timeout is appropriate there. But for a persistent Node.js server, 3 seconds is the ceiling.

Timer 2: idleTimeoutMillis

Postgres connections in the pool are reused across requests. When a request finishes, the connection returns to the pool and waits for the next caller. idleTimeoutMillis controls how long an idle connection stays in the pool before the pool closes it.

The default in pg is 10_000 (10 seconds). That is too short for most workloads.

const pool = new Pool({
  connectionTimeoutMillis: 3000,
  idleTimeoutMillis: 30000, // 30 seconds
  max: 20,
});

Why 30 seconds? A connection that is 10 seconds old is still warm (TCP state established, Postgres backend ready, prepared statements possibly cached). Closing it after 10 seconds means the next request spikes the pool to max because every connection was torn down. Then Postgres has to fork 20 new backends under load, which spikes CPU and latency.

The tradeoff is that idle connections consume Postgres server resources. Each idle backend takes about 5-10 MB of memory on the server side. With a max of 20, that is 100-200 MB for the idle pool. For most applications that is negligible. If you are running on a tiny Postgres instance (1 GB RAM) with hundreds of connections, lower the idle timeout to 10 seconds, but do not go lower than that.

The real risk with idleTimeoutMillis is setting it to 0, which disables idle connection recycling. With idleTimeoutMillis: 0, a connection that is checked back in stays in the pool forever. Over hours or days, TCP middleboxes (NAT gateways, load balancers, proxies) silently drop these idle connections. The pool thinks they are healthy. The first query on a stale connection gets a read ECONNRESET. The pool then retries the query on a fresh connection, adding 100-200 ms of latency to that one unlucky request. This is the most common intermittent timeout bug in production Node.js apps.

Always set a non-zero idle timeout. 30 seconds is a good starting point.

Timer 3: statement_timeout (Postgres-side)

Everything above controls the client-side behavior. This one controls the server side. statement_timeout is a Postgres configuration parameter that kills any query that runs longer than the specified number of milliseconds.

This is the most important timeout for preventing cascading failures. Without it, a single slow query can hold a connection for minutes. While it holds the connection, that connection is unavailable for other requests. The pool shrinks. Other requests queue up. Latency climbs. Queue depth grows. Memory usage rises. Eventually the process runs out of memory or the load balancer starts returning 502s.

Set it per-session or globally:

-- In postgresql.conf or ALTER SYSTEM
statement_timeout = '30s';

-- Or per-connection in the pool config
ALTER DATABASE appdb SET statement_timeout = '30000';

Or set it in the Node.js pool initialization by executing a startup query:

const pool = new Pool({
  connectionTimeoutMillis: 3000,
  idleTimeoutMillis: 30000,
  max: 20,
});

// Run this once on startup to set the default for all connections
pool.on('connect', (client) => {
  client.query("SET statement_timeout = '30s'");
});

30 seconds is a reasonable default for most web APIs. Adjust based on your slowest legitimate query. If you have a reporting endpoint that legitimately runs for 60 seconds, do not raise the global timeout. Use SET LOCAL statement_timeout inside that specific transaction:

async function runReport() {
  const client = await pool.connect();
  try {
    await client.query("SET LOCAL statement_timeout = '120s'");
    const result = await client.query(reportQuery);
    return result.rows;
  } finally {
    client.release();
  }
}

The key insight: statement_timeout protects the database from runaway queries. It is not a replacement for query optimization. If you are hitting it regularly, the fix is not to raise the limit. The fix is to EXPLAIN ANALYZE the query and add the missing index.

The lock_timeout companion

There is a related Postgres parameter you should set alongside statement_timeout: lock_timeout. This controls how long a query waits to acquire a lock before it is killed. Without it, a query blocked by ACCESS EXCLUSIVE from a ALTER TABLE or VACUUM FULL will sit on the connection until statement_timeout fires, even though it is doing zero work.

ALTER DATABASE appdb SET lock_timeout = '10s';

This prevents the common scenario where a migration or maintenance operation on one table blocks queries on unrelated tables (via relation extension locks) for minutes at a time.

Timer 4: TCP keepalives

TCP keepalives are the OS-level watchdog for dead connections. When a connection goes silent (server crashes without closing the socket, network partition), the keepalive mechanism detects it and closes the local socket, which triggers a clean error in the pool.

Postgres and node-postgres both support keepalive configuration, but the defaults are notoriously conservative. Linux defaults to 7200 seconds (2 hours) before the first keepalive probe. That means a dead connection stays in the pool for 2 hours before anyone notices.

const pool = new Pool({
  connectionTimeoutMillis: 3000,
  idleTimeoutMillis: 30000,
  max: 20,
  keepAlive: true,
  keepAliveInitialDelayMillis: 10000, // 10 seconds
});

This tells the OS to start sending keepalive probes 10 seconds after the last data was exchanged on the connection. If the server is unreachable, the OS will detect it within 20-30 seconds (three probes at 10-second intervals) and close the socket.

Without keepalives, a connection to a server that has been hard-restarted or had its network interface taken down will appear healthy to the pool until a query is attempted on it. The query then hangs for the connectionTimeoutMillis duration before the pool retries. During that hang, the connection is occupied and cannot serve other requests.

Set keepAlive: true and keepAliveInitialDelayMillis: 10000 on every pool. It costs nothing in normal operation and saves you from silent connection death.

Putting it all together: the production pool config

Here is the complete pool configuration that covers all four timers, with annotations for each setting:

const { Pool } = require('pg');

const pool = new Pool({
  // Connection
  host: process.env.PGHOST || 'localhost',
  port: parseInt(process.env.PGPORT || '5432', 10),
  user: process.env.PGUSER || 'app',
  password: process.env.PGPASSWORD,
  database: process.env.PGDATABASE || 'appdb',

  // Timer 1: How long to wait for a TCP connection
  connectionTimeoutMillis: 3000,

  // Timer 2: How long an idle connection stays in the pool
  idleTimeoutMillis: 30000,

  // Timer 4: TCP keepalive detection
  keepAlive: true,
  keepAliveInitialDelayMillis: 10000,

  // Pool sizing
  max: 20,
  min: 2,            // Keep 2 connections warm at all times

  // Reject queries after the pool is saturated
  maxUses: 7500,     // Recycle connections after 7500 uses
});

// Timer 3: statement_timeout set per-connection
pool.on('connect', (client) => {
  client.query("SET statement_timeout = '30s'");
  client.query("SET lock_timeout = '10s'");
});

// Log pool errors that don't have a specific query context
pool.on('error', (err) => {
  console.error('Unexpected pool error:', err);
});

This config gives you:

  • Fail fast on connection failure (3 seconds instead of 10 or infinite)
  • Warm pool with bounded churn (30-second idle timeout, 2 minimum connections)
  • Kill runaway queries (30-second statement timeout)
  • Detect dead connections (10-second keepalive probes)
  • Recycle leaky connections (maxUses prevents prepared statement plan cache bloat)

Testing your timeouts

A pool config is only useful if you verify the timeouts actually fire when expected. Here is a test that exercises each one:

const { Pool } = require('pg');
const assert = require('assert');

async function testTimeouts() {
  // Test 1: connection timeout to a non-routable IP
  const timeoutPool = new Pool({
    host: '10.255.255.1', // non-routable
    port: 5432,
    connectionTimeoutMillis: 2000,
    idleTimeoutMillis: 1000,
  });

  const start = Date.now();
  try {
    await timeoutPool.query('SELECT 1');
    assert.fail('Should have thrown');
  } catch (err) {
    const elapsed = Date.now() - start;
    assert.ok(elapsed >= 1900 && elapsed <= 4000,
      `Connection timeout should fire around 2s, took ${elapsed}ms`);
    console.log(`PASS: connection timeout in ${elapsed}ms`);
  }
  await timeoutPool.end();

  // Test 2: statement timeout with a slow query
  const statementPool = new Pool({
    host: process.env.PGHOST || 'localhost',
    database: 'testdb',
    connectionTimeoutMillis: 3000,
    idleTimeoutMillis: 1000,
  });

  await statementPool.query("SET statement_timeout = '500ms'");

  const start2 = Date.now();
  try {
    await statementPool.query('SELECT pg_sleep(10)');
    assert.fail('Should have thrown');
  } catch (err) {
    const elapsed = Date.now() - start2;
    assert.ok(elapsed >= 400 && elapsed <= 2000,
      `Statement timeout should fire around 500ms, took ${elapsed}ms`);
    console.log(`PASS: statement timeout in ${elapsed}ms`);
  }
  await statementPool.end();

  console.log('All timeout tests passed');
}

testTimeouts().catch(console.error);

Run this against a local Postgres instance or in your CI pipeline. If any timeout takes significantly longer than expected, your infrastructure layer (Docker networking, VPN, cloud firewall) may be swallowing timeout signals. That is important information to have before a production incident, not during one.

What happens when you get it wrong

Every timeout in this post maps to a real production failure mode I have seen or debugged:

Missing/Incorrect SettingFailure Mode
No connectionTimeoutMillisBrief DNS hiccup freezes all connection attempts. Pool saturates with waiting callers. API returns 502s for minutes after DNS recovers.
No idleTimeoutMillis (set to 0)Idle connections quietly die inside NAT gateways. First query on a stale connection gets ECONNRESET. Intermittent 200ms latency spikes.
No statement_timeoutOne missing index causes a sequential scan that takes 45 seconds. Every connection eventually runs the slow query. Pool locks up. Whole API goes down.
No TCP keepalivesServer hard-restarts. Pool thinks all connections are healthy. First query after restart waits 120 seconds for OS to detect dead socket. Every request queues.

The common thread in every case is that the application did not fail fast. It waited. And waiting turned a 4-second blip into a 12-minute recovery.

The takeaway

Database connection timeout configuration is not a set-and-forget detail. It is the primary defense against cascading failures in any application that depends on Postgres. The defaults in pg are safe for a development environment but unsuitable for production. They tolerate too much uncertainty before failing, and that tolerance amplifies small infrastructure problems into system-wide outages.

Set all four timers. Test them in CI. Monitor pool.waitingCount in production (the number of queries waiting for a connection). If waitingCount ever exceeds zero during normal traffic, your pool is too small or your timeouts are too long.

The goal is not to prevent failures. The goal is to make failures fast, visible, and isolated. A query that times out in 3 seconds and returns a 503 to the caller is better than a query that hangs for 30 seconds, consumes a connection, and blocks 19 other requests behind it.

Fail fast, recover faster.


A note from Yojji

Getting the infrastructure-level details right — connection timeouts, pool sizing, keepalive intervals — is the kind of backend engineering that does not show up in feature demos but determines whether your application survives its first production incident. Yojji’s teams build Node.js and Postgres systems for startups and enterprises alike, and they treat these configuration details as first-class design decisions, not afterthoughts. Their senior engineers bring the same discipline to timeouts, connection management, and database operations that they bring to the application logic itself.

Yojji is an international custom software development company founded in 2016, with offices in Europe, the US, and the UK. They offer full-cycle product development and dedicated team augmentation, specializing in the JavaScript ecosystem, AWS/Azure/GCP infrastructure, and microservices architectures.