Wood duck standing among felled logs and blue and purple cables on a forest floor.

Most of the log_* cluster is about what happened inside a session. These three are about the session itself: who showed up, when, how long it took to let them in, how long they stayed, and (if you insist) what their machine was called. The defaults log none of it. That is a defensible choice for a laptop and the wrong one for any server you can be paged about.

log_connections

The default is '', the empty string, meaning off. Through PostgreSQL 17 this was a boolean. PostgreSQL 18 made it a comma-separated list of the stages of connection setup you want logged: receipt, authentication, authorization, and setup_durations, with all as shorthand for the four. The old spellings (on, off, true, 1, and friends) still work by themselves; on means receipt,authentication,authorization, which is exactly what a pre-18 server logged. A compatibility token mixed with the new options ('on,setup_durations') is rejected, so configuration management that templates on needs to learn the list.

Here are the four stages on 18.6, for a scram-sha-256 login over TCP, with log_line_prefix = '%m [%p] %q%u@%d %r ' (timestamps and the pg_hba.conf path trimmed):

1[5591] [unknown]@[unknown] 127.0.0.1(55432) LOG: connection received: host=127.0.0.1 port=55432
2[5591] postgres@postgres 127.0.0.1(55432) LOG: connection authenticated: identity="postgres" method=scram-sha-256 (pg_hba.conf:1)
3[5591] postgres@postgres 127.0.0.1(55432) LOG: connection authorized: user=postgres database=postgres application_name=psql
4[5591] postgres@postgres 127.0.0.1(55432) LOG: connection ready: setup total=8.079 ms, fork=0.376 ms, authentication=6.310 ms

receipt is written by the freshly forked backend before it has read a byte from the client, which makes it the only line a connection is guaranteed to leave behind. A client that opens a socket and never sends a startup packet is killed by authentication_timeout via _exit(1), with no message at all; the connection received line is the entire record that it happened. authentication (added in 14) records the identity the authentication method actually saw and the pg_hba.conf line that matched, which is the audit trail you want with LDAP, GSSAPI, or client certificates and mostly redundant with SCRAM. Failed authentication is logged whether or not this is on. authorization is the “you’re in” line: database, application_name, and the TLS or GSS details if any.

setup_durations is new in 18 and is the reason to revisit this parameter when you upgrade. The total runs from the postmaster’s accept() to the backend’s first ReadyForQuery. Fork is the fork. Authentication brackets the exchange with the client. Everything else lands in the gap between the total and the other two: the TLS handshake, waiting for the client to send its startup packet, catalog initialization, and (see below) reverse DNS. The 6 ms of authentication above is normal for SCRAM; most of it is the client grinding through its 4096 rounds of PBKDF2, plus the round trips. Hundreds of milliseconds there means an external authentication server (LDAP, RADIUS, Kerberos) is in the loop and slow, or the client is a long way away. A big gap with a small fork and a small authentication is the client dawdling, a slow TLS handshake, or a resolver.

The context is superuser-backend, and the documentation’s claim that it can be changed at session start is more generous than the mechanics. ALTER ROLE ... SET and ALTER DATABASE ... SET are rejected outright. You can pass it in the startup packet (PGOPTIONS='-c log_connections=...') as a superuser or a role with SET privilege on the parameter, but startup options are applied after authentication and authorization are finished, so from the client side the only stage you can suppress is setup_durations; the other three are already in the log. A role without the privilege that tries it doesn’t get a quieter session. It gets FATAL: permission denied to set parameter and no session. In practice this parameter lives in postgresql.conf, takes effect on reload for new sessions, and that is the whole story.

log_disconnections

Default off, context superuser-backend with the same caveats, and one line per session end:

1LOG: disconnection: session time: 0:00:00.008 user=postgres database=postgres host=127.0.0.1 port=55432

The session time is the field that matters. It’s the cheapest connection-pool audit there is: a log full of sessions that lived for eleven milliseconds is an application opening a connection per query, and you’ll find it in the disconnection lines long before anyone catches it in pg_stat_activity at the right instant. It runs as an on_proc_exit handler, so it fires for normal exits, FATAL errors, and pg_terminate_backend(), and not for a backend that died by signal 9 (nothing does).

log_hostname

Default off, context sighup. Leave it off.

The documentation says it “might impose a non-negligible performance penalty.” The mechanics are worse than that sentence. With it on, every new backend, immediately after fork and before it does anything else, calls getnameinfo() on the client’s address and blocks until the resolver answers. That call runs before authentication_timeout is armed; the source says so in a comment, in case you were counting on the timeout. If the PTR record simply doesn’t exist, you get the numeric address back and lose only a round trip. If the resolver is unreachable, glibc’s defaults (resolv.conf(5): five-second timeout, two attempts) stall every new connection for ten seconds per unreachable nameserver listed, and the only knob that shortens it is the resolver configuration on the database host. When the lookup finally gives up, getnameinfo() returns an error rather than a miss, and PostgreSQL substitutes ??? for both the host and the port. Every line that session writes, %r in the prefix and the disconnection line included, now says ???(???). You turned on a parameter to learn more about the client, and the outage took away the IP address you already had.

The one case where it’s nearly free is a pg_hba.conf that already matches on hostnames: that lookup happens anyway during authentication, and log_hostname merely moves it earlier and caches the result. Everywhere else, log the address and resolve it at analysis time, when a slow DNS server costs you a slow report instead of a slow login. (It also populates pg_stat_activity.client_hostname, which is null otherwise. That column is not worth this.)

So, on 18: log_connections = 'all' and log_disconnections = on, in postgresql.conf, reload, done. On 14 through 17 it’s on and on. If the resulting log volume bothers you, the volume is the finding: that many connections per second with nothing pooling them is the problem to fix, and the log is how you found out.