← Debugging Guides

阅读中文版本

PostgreSQL Connection Reset — Server Closed the Connection Unexpectedly

Diagnose PostgreSQL connection resets after idle periods by comparing backend logs, idle session age, and infrastructure timeouts, then fix keepalive and pool lifetime.

intermediatedebuggingpostgresqlnetworkingconnection-pool

Symptoms

  • server closed the connection unexpectedly
  • could not receive data from server: Connection reset by peer
  • The first query after a quiet period fails, while the next connection works
  • FATAL: terminating connection due to administrator command appears in the server log

Possible causes

  • A NAT table entry, firewall, or load balancer expired the idle TCP flow without notifying either endpoint
  • The backend was ended by pg_terminate_backend, a restart, failover, or the OOM killer
  • The connection pool keeps sockets open longer than the infrastructure idle timeout
  • A pooler such as PgBouncer closes server connections that exceed its server lifetime

Diagnosis steps

Read the server log around the failure timestamp

grep -E 'terminating connection|server process .* was terminated|connection reset' /var/log/postgresql/postgresql-16-main.log | tail -50

Interpretation

A matching entry for the backend PID means the server closed the session. No entry at the same second points to a network device dropping the idle flow.

Check whether the cluster restarted or failed over

SELECT pg_postmaster_start_time(), now() - pg_postmaster_start_time() AS uptime;

Interpretation

A short uptime explains resets across all pools at once. It also means the lock and buffer cache state was lost.

Compare idle session age with the infrastructure timeout

SELECT pid, client_addr, state, now() - state_change AS idle_for, left(query, 60) FROM pg_stat_activity WHERE state = 'idle' ORDER BY idle_for DESC LIMIT 20;

Interpretation

Many idle sessions just under a round number such as 300 or 350 seconds indicate a NAT or load balancer idle timeout.

Look for an out-of-memory kill of a backend

dmesg -T | grep -iE 'killed process|out of memory' | tail -20

Interpretation

A killed postgres process explains resets during memory pressure and needs memory work, not only reconnects.

Root cause

The TCP socket was closed after PostgreSQL had already authenticated the client. Either the server ended the backend, or an idle timeout on the network path discarded the flow without a FIN reaching the client. The client only learns about it when it reuses the dead socket, so the reset surfaces as a statement failure rather than a connect failure.

Fix

Enable TCP keepalives on the server and in libpq, and set a pool maximum lifetime below the smallest idle timeout on the path. Reconnect on SQLSTATE 08006 before retrying a statement, and retry only operations that are safe to repeat. Fix the underlying restart, OOM, or pooler lifetime instead of masking it.

Risk note

Retrying a statement that may already have committed can duplicate writes. Terminating backends yourself aborts transactions and can leave application state inconsistent. Keepalive shortens detection time but does not repair a connection in the middle of a transaction.

Verify the fix

Leave the pool idle longer than the load balancer timeout, then run a query and confirm the client opens a new connection without a reset. Correlate client errors with server log timestamps over several cycles, and check that idle session age stays below the pool lifetime.

A reset is a transport failure, not a SQL error

server closed the connection unexpectedly and could not receive data from server: Connection reset by peer arrive after PostgreSQL has authenticated the client. The socket the pool believes is healthy is already closed, so the next statement fails and the following checkout may succeed.

FATAL:  terminating connection due to administrator command
could not receive data from server: Connection reset by peer

Two actors produce this: the server ending the backend (restart, failover, pg_terminate_backend, OOM killer) or the network path discarding an idle flow. The server log for the backend PID is the discriminator.

Find who closed the socket

A recent pg_postmaster_start_time() means every pool was cut at once. now() - state_change for idle sessions exposes a suspiciously uniform age: when connections die at almost the same interval, a NAT or load balancer idle timeout is the likely cause. Pooler settings such as server_idle_timeout and server_lifetime can also close server connections that the application pool still references.

Make reconnection deterministic

Set keepalives so a dead peer is detected before reuse, and cap pool lifetime below every idle timeout on the path.

const pool = new Pool({
  max: 10,
  keepAlive: true,
  idleTimeoutMillis: 30_000,
  maxLifetimeSeconds: 240, // below the load balancer idle timeout
});

In libpq, keepalives_idle, keepalives_interval, and keepalives_count control probing. When a checkout fails before the statement is sent, SQLSTATE 08006 is safe to reconnect and retry once; a transaction that may have committed is not.

Verify the lifecycle

Idle the pool past the infrastructure timeout, run a query, and watch pg_stat_activity: the old backend disappears and a new one appears without a client reset. Repeat several cycles, then compare client error counts with server log entries at the same timestamps.