Blog
PostgreSQL logging

The logging setting that slows down new connections

The name log_hostname suggests that this setting puts the client into PostgreSQL's server log. It does not: the client address comes from %h in log_line_prefix. What log_hostname adds is a DNS lookup at the start of every new connection. We explain how the two settings work together, and we show what that lookup cost on PostgreSQL 18 when DNS was healthy, slow and down.

2026-10-02 10:06:26.730 UTC [7207] [unknown]@[unknown]/[unknown] 10.42.1.5 LOG:  connection received: host=10.42.1.5 port=60038
2026-10-02 10:06:26.742 UTC [7207] bench@postgres/[unknown] 10.42.1.5 LOG:  connection authorized: user=bench database=postgres application_name=pgbench SSL enabled (protocol=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384, bits=256)
2026-10-02 10:06:26.742 UTC [7207] bench@postgres/pgbench 10.42.1.5 LOG:  connection ready: setup total=13.170 ms, fork=0.513 ms, authentication=6.245 ms
Three lines from one connection in our test. The log line prefix ends with %h, and the circled address is what %h prints: the IP address of the client that connected, not of the server. PostgreSQL logged it without any DNS lookup.

Two settings that work together

The server log can show where each connection came from. Two settings decide how, and they do different jobs.

The PostgreSQL 18 documentation describes log_hostname like this: "By default, connection log messages only show the IP address of the connecting host. Turning this parameter on causes logging of the host name as well. Note that depending on your host name resolution setup this might impose a non-negligible performance penalty."

Two details matter in practice. First, log_hostname adds no field of its own. Without %h - or %r, which adds the port - in log_line_prefix, ordinary log lines do not show the client at all, whatever log_hostname says, and the upstream default prefix, '%m [%p] ', has neither. The setting also changes the host= field of the "connection received" message, which appears when connection logging is on, and it fills the client_hostname column of pg_stat_activity. Second, in our tests the host name replaced the IP address in the log. It did not appear next to it.

What the lookup is, and when it happens

A reverse DNS lookup asks DNS which name belongs to an IP address (a PTR record). With log_hostname on, PostgreSQL makes this lookup for every new TCP connection, right after the server process for that connection starts - before the client's first message is read and before authentication. The connection waits until DNS answers, or until the system's resolver gives up. Connections over a local Unix socket make no lookup.

The lookup runs whenever the setting is on, also when nothing in your configuration prints the name. Its time does not count against authentication_timeout, either. A comment next to the lookup in the PostgreSQL 18 source code says so directly: "PreAuthDelay and any DNS interactions above don't count against the time limit."

What it cost with healthy DNS

We measured the cost on two virtual machines in Microsoft Azure, in one private network: PostgreSQL 18.6 on Ubuntu 24.04 (Standard_D4s_v5, 4 vCPUs) and a separate client machine (Standard_D2s_v5). Connections used TLS 1.3 and SCRAM-SHA-256 password authentication, as many production setups do. The client ran pgbench, the benchmark tool that ships with PostgreSQL, with a one-line custom script (SELECT 1) and the -C option, which opens a new connection for every transaction. That makes connection setup most of the work, which is what this test is about. It is not a throughput test of a real workload.

Azure's DNS service answered the lookup for the client's address with the name vm-logtalk-hostname-cli.internal.cloudapp.net. Ubuntu runs a local DNS cache by default (systemd-resolved). Without such a cache, every lookup goes to the DNS server, so we tested the database server both ways.

For each setting, the table shows two numbers that pgbench prints at the end of a run. Connection time is how long it took, on average, to open one connection. Connections per second is how many times per second the client connected, ran SELECT 1 and disconnected; pgbench reports it as transactions per second, and with -C every transaction is one connection. With one client, each connection starts only after the previous one has finished, so the two numbers move in opposite directions: when every connection takes longer, fewer of them fit into a second. Connections per second here is the pace of a single client connecting again and again, not the most the server could accept.

Median of three 30-second pgbench runs per setting, run in alternating order: one client, a new connection for every transaction (pgbench -C), PostgreSQL 18.6 on Azure, Azure DNS answering normally. Connection time is pgbench's "average connection time"; the range shows the lowest and the highest of the three runs. Connections per second is pgbench's "tps (including reconnection times)".
log_hostname DNS lookups Connection time
(lower is better)
Connections per second
(higher is better)
off (the default)none14.1 ms (14.1-14.2)67.8 (baseline)
onthrough a local cache14.4 ms (14.3-14.5)66.6 (-1.8%)
onstraight to Azure DNS, no local cache20.2 ms (19.9-20.2)48.1 (-29%)

Without a local cache, every new connection took about 6 ms longer: 43% more time per connection, and 29% fewer connections per second in this test. The server's own timing agrees. PostgreSQL 18 can log how long each connection took to set up (log_connections with the setup_durations option): the average grew from 10.4 ms to 16.3 ms, while the authentication part stayed at 6.0 ms in all three variants. The extra time is spent before authentication, where the lookup happens.

With the local cache, the difference was 0.3 ms. Azure DNS gave this answer a lifetime (TTL) of 10 seconds, so most lookups came from the cache. The 6 ms is what one round trip to Azure DNS cost from this server. On your network it is a different number: the time your DNS server needs to answer.

What it cost with slow or failing DNS

Healthy DNS is the best case. We then slowed DNS down, and then made it unreachable - only for the database server and only for DNS traffic - and ran the same test with one client.

One run per scenario: 30 seconds, 60 seconds for the run with DNS down and log_hostname on. One client, a new connection for every transaction, no local DNS cache unless the row says so. "DNS does not answer" means that the DNS server's answers never reached the database server, as in an outage. Connection time is pgbench's "average connection time"; in each run, the server's timing of the individual connections stayed close to this value.
DNS log_hostname = off log_hostname = on
answers 100 ms late14.0 ms122.6 ms
does not answer14.2 ms20,036 ms (20 seconds)
does not answer, local cachenot measured - no lookup10,027 ms (10 seconds)

With log_hostname off, DNS problems made no difference, because there was no lookup. With it on, every new connection waited for DNS. When DNS answered 100 ms late, each connection took about 100 ms longer. When DNS did not answer, each connection waited until the resolver gave up: 20 seconds when the system resolver (glibc 2.39 on Ubuntu 24.04) asked DNS directly - it sent the question four times, five seconds apart - and 10 seconds through systemd-resolved. Resolver options can change these numbers, but not the pattern: a new connection takes as long as DNS takes to answer or to fail. With eight clients connecting at the same time and DNS down, the server completed 0.2 new connections per second. With log_hostname off and DNS healthy, the same eight clients completed 160 per second.

When the lookup fails, the IP address is lost too

These are three lines from one of those 20-second connections, with %h in the log line prefix. The timestamp and process ID at the start of each line are replaced with ..., and long lines are wrapped:

... [unknown]@[unknown]/[unknown]  WARNING:  pg_getnameinfo_all() failed:
      Temporary failure in name resolution
... [unknown]@[unknown]/[unknown] ??? LOG:  connection received:
      host=??? port=???
... bench@postgres/pgbench ??? LOG:  connection ready:
      setup total=20033.681 ms, fork=0.540 ms, authentication=5.641 ms

When the lookup fails, PostgreSQL logs a WARNING and writes ??? in place of the client's host and port. The IP address the server already had does not appear anywhere in the log. It stays visible only in pg_stat_activity.client_addr, and only while the session is open. The last line also shows where the time went: 20 seconds of setup, of which authentication took 5.6 ms.

The cost does not depend on what you log. In one run we used the upstream default log_line_prefix and turned connection logging off, so nothing in the log could show a host name. Each connection still waited 20 seconds, and the only trace in the log was one WARNING line per connection.

Connection pools hide the cost until they reconnect

The lookup happens once per connection, not once per query. In one 30-second run per setting with eight connections that stayed open, the server answered 19,291 queries per second with log_hostname off and 19,165 with it on. One run per setting cannot show whether a 0.7% difference is real, but the mechanism explains why it should be small: once a connection exists, it makes no more lookups.

This is why a connection pool can hide the setting for a long time. The cost comes back when the pool opens new connections - for example after a database restart or a failover, when every connection in the pool is opened again at about the same time, and each one makes its own lookup.

How to check your server

Connect to your database with psql or any other SQL client you normally use, and run:

SELECT name, setting, source
FROM pg_settings
WHERE name IN ('log_hostname', 'log_line_prefix', 'log_connections');

To turn it off - in an SQL session, the same way as above, connected as a superuser:

ALTER SYSTEM SET log_hostname = off;
SELECT pg_reload_conf();

The change needs a reload, not a restart, and applies to connections opened after the reload. To get the client address on every log line, add %h to log_line_prefix after %q. %q itself prints nothing: it tells background processes, which have no client, to stop at that point of the prefix. For example, in an SQL session as a superuser:

ALTER SYSTEM SET log_line_prefix = '%m [%p] %q%u@%d/%a %h ';
SELECT pg_reload_conf();

This prefix only demonstrates where %h goes; it is not a recommendation for your whole prefix. Keep the prefix you have and add %h to it, and update every tool that reads your log - log analyzers, alert rules - so it knows the new format.

To confirm the result, open a new connection and check that its log lines show the IP address in the prefix. On PostgreSQL 18 you can also measure connection setup on your own server: when log_connections includes setup_durations, the server logs "connection ready: setup total=..." for every new connection. That is one more log line per connection, so use it for a measurement window, not forever, on servers that open many connections.

On managed services, PostgreSQL settings live in the provider's console - a parameter group on Amazon RDS, server parameters on Azure Database for PostgreSQL, database flags on Google Cloud SQL - and the provider decides which of them you can change. Check what your server actually runs with the query above.

If you need host names

Sometimes a name is useful - for example, when client IP addresses change often and you keep logs for a long time. In that case, resolve the addresses when you read the log, not while clients wait to connect: an IP address in the log can be looked up later, during an investigation. Keep in mind that the answer then reflects DNS at the time you ask, not at the time of the connection.

Two more notes. Behind a connection pooler, %h shows the pooler's address, whatever log_hostname says. And to see which application a log line belongs to, use application_name: the client sets it, %a prints it, and it needs no lookup.

How we tested

Two Azure virtual machines in the Poland Central region, in their own private network: the database server on Standard_D4s_v5 (Ubuntu 24.04, glibc 2.39, PostgreSQL 18.6 from the PostgreSQL project's packages) and the client on Standard_D2s_v5. PostgreSQL settings as installed by the packages, apart from the log line prefix, connection logging, log file handling and the listen address. Client-side numbers come from pgbench 18.6; server-side numbers from the PostgreSQL 18 log message "connection ready: setup total=..." of the benchmark's own connections. Slow DNS: 100 ms of delay added to every packet from the database server to Azure DNS (tc netem). DNS down: Azure DNS answers dropped at the database server's firewall (iptables), so the questions left and no answer came back. The healthy-DNS variants ran three times each, in alternating order; the slow, failing, eight-client and persistent-connection scenarios ran once each. The local DNS cache was emptied before every run.

Animation in five rows. Each row shows a client, the PostgreSQL server and a DNS server. Off: %h prints the client's IP address and there is no DNS lookup; 14 ms per connection. On: one reverse DNS lookup for each new connection, even if no log line prints %h, and %h prints the name; 20 ms per connection. DNS down: each new connection waits until DNS gives up, 20 seconds, and %h prints ???. Connection pool: one lookup when a connection opens, none for each query; after a failover, every connection asks again. Reload: a session that is already open keeps the old %h, and only new sessions print the IP address.
The cases from this article in one picture. Times are from our test: pgbench -C, no local DNS cache.

In short

Get started

Want a real say in what we build?

Guardioz is in beta. You can sign up, and the product is early - we say so up front. Always free for one user. No credit card required. Every customer gets a real say in what we build next: which problems matter most, which recommendations earn trust, where the guardian goes from here.

Questions first? Write to .