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.
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 -50Interpretation
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 -20Interpretation
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.