idle_session_timeout is housekeeping, not protection. Its sibling, idle_in_transaction_session_timeout, defends the database from actual harm: an idle transaction holds locks and pins the xmin horizon, and vacuum suffers for it. A session idle outside a transaction does none of that. It ties up a connection slot, a backend process, and some memory. That is the entire bill, and this parameter exists to collect it. The documentation says as much, noting that an idle session without a transaction imposes no large costs and so there is less need for this timeout than for its sibling.

The default is 0, meaning disabled; the context is user; the value is in milliseconds, with the usual suffixes accepted (30min, 2h). It arrived in PostgreSQL 14. Set it to a nonzero value, and any session idle outside a transaction for longer than that is terminated with FATAL: terminating connection due to idle-session timeout, SQLSTATE 57P05. That SQLSTATE is a small piece of good taste: the error was born in class 08 (Connection Exception), and Tom Lane moved it to class 57 (Operator Intervention) during the 14 cycle, next to admin_shutdown, on the theory that from the client’s perspective this is indistinguishable from an administrator killing the session. Client stacks that already treat class 57 as “reconnect and move on” handle it correctly for free.

The two idle timers are mutually exclusive by construction. When a backend goes idle, exactly one of them arms: this one if there is no open transaction, the sibling if there is (including a failed one). The timer fires at expiry, not lazily at the next command; the backend sends the FATAL and exits. Whether the client notices immediately depends on whether it is watching the socket. Interactive psql is not; it discovers the corpse on your next command, prints the error, and reconnects on its own. The reconnect is a new session, so your SETs, temporary tables, and prepared statements are gone. Application drivers surface it as the familiar “server closed the connection unexpectedly.”

What it is for: leaked connections. The psql session forgotten in a tmux window since Tuesday. The GUI client holding twelve connections because someone opened twelve tabs. The application without a pooler that opens connections and loses track of them. Each one occupies a max_connections slot, and when the slots run out at 3 a.m. (it is always 3 a.m.), these are exactly the sessions someone ends up killing by hand.

Note what it is not. It is not a dead-client detector; a client that vanished without closing its socket is the department of tcp_keepalives_idle and client_connection_check_interval. This parameter evicts clients that are alive and simply have nothing to say. It is also not enforcement: the context is user, so any role can SET idle_session_timeout = 0 and opt out. This is hygiene for cooperative clients, not a security boundary against uncooperative ones.

And one warning in bold, because people keep doing it: do not set this globally on a database behind a connection pooler. PgBouncer holds idle server connections on purpose; that is what a pool is. Reaping them server-side fights the pooler and generates a steady drizzle of reconnect errors, and it accomplishes nothing, because the idle connections you actually care about are on the client side of the pooler, where this parameter cannot see them. PgBouncer has its own knob for its own idle servers (server_idle_timeout); use that.

Before enabling anything, find out who is actually leaking:

1SELECT usename, application_name,
2 count(*) AS sessions,
3 max(now() - state_change) AS longest_idle
4FROM pg_stat_activity
5WHERE state = 'idle'
6GROUP BY 1, 2
7ORDER BY sessions DESC;

If the answer is “the pooler,” stop; that is the pooler doing its job. If the answer is humans and their tools, set it per role and leave the global default alone:

1ALTER ROLE some_human SET idle_session_timeout = '2h';

Two hours survives lunch and a meeting, and it means Friday afternoon’s abandoned session is not still holding a slot on Monday. The global setting stays at 0. The connections worth reaping belong to people, and people can be enumerated.