You see server DNS lookup failed in the PgBouncer log, usually right after a PostgreSQL failover, a DNS change, or a network event. New server connections cannot be established. Depending on timing, you may instead see the nastier variant: no error at all, just a pool quietly draining because PgBouncer’s DNS cache still points at the old primary IP.
PgBouncer maintains its own DNS cache, independent of the TTL your DNS records publish. The cache lifetime is controlled by dns_max_ttl (default: 15 seconds). After a failover, up to dns_max_ttl can pass before PgBouncer even becomes eligible to re-resolve the backend hostname. Cached results are only re-queried when a new server connection is needed, so existing connections to the old IP keep running (against a dead or read-only host) while nothing forces a fresh lookup.
This article covers how to confirm the stale-cache condition, how to force recovery, and how to tune DNS behavior so the next failover does not page you.
What this means
PgBouncer resolves backend hostnames asynchronously and caches the result. Two failure modes follow:
- Lookup fails outright. The resolver returns an error or NXDOMAIN. PgBouncer logs
server DNS lookup failedand cannot create new server connections. DNS errors and NXDOMAIN results are themselves cached fordns_nxdomain_ttl(default: 15 seconds), so a transient resolver hiccup suppresses retries for that window. - Lookup succeeds but returns the stale IP. The cache still holds the pre-failover address. New connections go to the old primary, which is dead, refusing connections, or demoted to standby and read-only. Applications see connection failures or read-only transaction errors. Connections established before the failover may keep working briefly, which masks the problem while the pool drains.
The second mode is the classic “failed failover” pattern: the database layer failed over correctly, but the pooler did not follow. PgBouncer has no health-based backend routing. It connects wherever the cached DNS answer says.
When a hostname’s resolution does change, existing server connections using the old address are closed as they are released back to the pool (timing depends on pooling mode), and new connections use the new address. Nothing force-closes active connections mid-transaction. That is what RECONNECT and RELOAD are for.
flowchart TD
A[PostgreSQL failover - IP changes] --> B[DNS record updated]
B --> C{PgBouncer DNS cache}
C -->|stale entry within dns_max_ttl| D[old IP returned]
C -->|lookup fails| E[server DNS lookup failed in log]
D --> F[new connections hit dead or read-only host]
E --> G[no new server connections]
F --> H[pool drains, sv_idle falls]
G --> H
H --> I[cl_waiting grows until query_wait_timeout]
J[RELOAD or RECONNECT] --> K[cache flushed, connections re-established]
K --> CCommon causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Stale cache after failover | Failover completed, but SHOW DNS_HOSTS still shows the old IP; new connections fail or go read-only | SHOW DNS_HOSTS resolved addresses vs current DNS answer |
| DNS resolver unreachable or slow | server DNS lookup failed in log, dns_queries in SHOW LISTS stays above zero | Test resolution from the PgBouncer host with the same resolver PgBouncer uses |
| NXDOMAIN or error cached | A transient resolver failure got cached for dns_nxdomain_ttl, blocking retries | SHOW DNS_HOSTS; wait out or flush the cache |
| dns_max_ttl too high for your failover RTO | Every failover takes up to dns_max_ttl plus connection drain time to recover | SHOW CONFIG for dns_max_ttl |
| Hostname config drift | Backend configured by hostname where you expected an IP, or an /etc/hosts entry that was not updated after failover | The [databases] section of pgbouncer.ini and /etc/hosts |
| AAAA lookup problems | PgBouncer requests both A and AAAA records; some DNS backends error on AAAA (for example when IPv6 is disabled), failing the whole lookup | Test both A and AAAA queries for the backend hostname against the same resolver |
Quick checks
All checks run against the PgBouncer admin console and are read-only unless noted.
# See what IP PgBouncer has cached for each backend hostname,
# and how many seconds until the entry is eligible for re-query
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -c "SHOW DNS_HOSTS;"
# Compare against the current DNS answer from the same host
getent hosts pg-primary.example.com
# or
dig +short pg-primary.example.com
# Check whether DNS queries are stuck in flight
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -Atc "SHOW LISTS;" | grep dns
# Check pool state: is the pool draining?
# Falling sv_idle + rising sv_login + growing cl_waiting = connections not being replaced
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -c "SHOW POOLS;"
# Confirm the TTL and resolver config PgBouncer is running with
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -Atc "SHOW CONFIG;" | grep -i dns
# Look for the error and related connection failures in the log
grep -E "server DNS lookup failed|connect failed|login failed" /var/log/pgbouncer/pgbouncer.log | tail -30
Key things in SHOW DNS_HOSTS output: the resolved addresses per hostname, and the TTL column (seconds until the entry is eligible for re-lookup). If the address shown there does not match what dig returns from the same host, you have confirmed the stale-cache condition.
How to diagnose it
- Confirm the symptom class. Grep the log for
server DNS lookup failed. If present, resolution itself is failing. If absent but applications report connection errors or read-only errors after a failover, suspect the stale-IP variant instead. - Read the cache. Run
SHOW DNS_HOSTS. Write down the resolved address for each backend hostname. - Read reality. Resolve the same hostname from the PgBouncer host (
dig +short,getent hosts). Use the resolver PgBouncer is configured to use, not just the system default, ifresolv_confpoints elsewhere. - Compare. If the cached IP differs from live DNS, the cache is stale. If live DNS itself still returns the old IP, the problem is upstream of PgBouncer: the failover tooling did not update DNS, or the record TTL at your DNS layer is the bottleneck.
- Check pool impact. Run
SHOW POOLS. The signature of a DNS-blocked pool is total server connections declining over time (sv_idle falling, sv_login stuck or failing) whilecl_waitingandmaxwaitgrow. Distinguish this from ordinary pool exhaustion, where total connections stay pinned atpool_size. See PgBouncer backend unreachable: PostgreSQL down and the pool draining for the broader pattern. - Check for in-flight queries.
SHOW LISTSand look atdns_queries. Consistently non-zero means the resolver is slow or unreachable, not just that the cache is stale. - Check both address families. If your DNS backend errors on AAAA queries, test explicitly:
dig AAAA <hostname> @<resolver>. An AAAA error can fail the entire lookup even when the A record is fine.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
SHOW DNS_HOSTS resolved addresses | Ground truth for where PgBouncer will connect | Cached IP does not match live DNS after a topology change |
dns_queries (SHOW LISTS) | In-flight DNS lookups; resolver health proxy | Sustained above zero |
sv_login (SHOW POOLS) | Connections stuck or failing during backend establishment | Persistently above zero with low sv_active |
| Total server connections per pool | A draining pool means connections die without replacement | sv_active + sv_idle + sv_used declining over minutes |
cl_waiting and maxwait (SHOW POOLS) | User-facing impact once the pool can no longer serve requests | cl_waiting > 0 sustained, maxwait climbing toward query_wait_timeout (default 120s) |
Log: server DNS lookup failed, connect failed | The only direct error signal; PgBouncer has no error counters in SHOW commands | Any occurrence during or after a failover |
Fixes
Wait out the TTL
If dns_max_ttl is at its 15-second default and the failover just happened, the cache entry expires on its own and the next needed server connection triggers a fresh lookup. Acceptable only if your RTO tolerates the wait plus connection drain time. The TTL expiring only makes the entry eligible for re-query. The actual re-query happens when a new server connection is needed, and existing connections to the old IP are closed as they are released, not immediately.
RELOAD to flush the DNS cache
# Disruptive: re-reads config and re-resolves all hostnames
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -c "RELOAD;"
RELOAD re-reads the configuration and forces re-resolution of backend hostnames. This is the standard recovery action when the cache is stale. It is not free: it re-processes the full config, and on versions before 1.24.0 it also recycled TLS connections, causing a noticeable reconnect wave. On 1.24.0 and later, TLS connections are only recycled if TLS settings actually changed.
RECONNECT to force new server connections
# Disruptive: closes all server connections for the database
# after they finish their current transaction
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -c "RECONNECT mydb;"
# or all databases
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -c "RECONNECT;"
RECONNECT (available since PgBouncer 1.11) closes server connections as they finish their current transaction, forcing replacements that re-resolve DNS. Use this when you have flushed or expired the cache but long-lived connections are still pinned to the old IP. In session pooling mode, connections are held for the whole client session, so drain time depends on client behavior. Expect a brief latency bump as new connections authenticate.
Fix the resolver path
If the lookup itself is failing rather than returning stale data: verify the resolver PgBouncer uses is reachable, check resolv_conf if you point PgBouncer at a non-default resolver file, and test AAAA behavior if your DNS backend is picky. A cached NXDOMAIN clears after dns_nxdomain_ttl (default 15s), or immediately on RELOAD.
Prevention
- Set
dns_max_ttldeliberately. The 15-second default is reasonable for many setups, but if your failover mechanism is DNS-based, aligndns_max_ttlwith your RTO. Lower values speed failover at the cost of more resolver load, which is usually trivial for a handful of backend hostnames. - Consider
dns_zone_check_period. When set (default: 0, disabled), PgBouncer polls the zone SOA serial and re-queries hostnames when it changes, giving faster pickup than TTL expiry alone. It requires the c-ares DNS backend, and it does not help if your hostname is a CNAME chain into another zone, since the authoritative zone cannot be determined. - Runbook the recovery commands. Failover automation should include
RELOADor targetedRECONNECTagainst PgBouncer as a post-promotion step, so recovery does not depend on someone remembering the cache exists. - Prefer IPs or local resolution where DNS adds no value. If a backend address is effectively static, configuring an IP (or a managed
/etc/hostsentry) bypasses the cache entirely. You trade failover flexibility for one less moving part. - Test the failover path end to end. The database failover working is not the same as the pooled path recovering. Drill it: promote a replica, watch
SHOW DNS_HOSTS, measure time-to-recovery through PgBouncer. - Watch the signals, not just the log. Alert on draining pool totals plus rising
cl_waitingduring known failover windows, and checkpaused/disabledstate before escalating, since maintenance produces lookalike symptoms.
How Netdata helps
- Netdata collects PgBouncer pool state (
cl_waiting,maxwait,sv_active,sv_idle,sv_login) at per-second granularity, so the drain pattern of a stale-DNS event is visible as it develops, not afterquery_wait_timeoutstarts firing. - Per-pool breakdowns show whether the problem is isolated to one
(database, user)pool or hitting every backend, which separates a single stale hostname from a resolver-wide failure. - Correlating
sv_loginagainst total server connections distinguishes “connections failing to establish” (DNS or backend problem) from “pool saturated but healthy” (ordinary exhaustion), the two most commonly confused PgBouncer incidents. - High-resolution
maxwaithistory tells you how close waiters came toquery_wait_timeoutduring the event, which informs whether yourdns_max_ttland failover runbook actually meet your RTO. - Log-based signals like
server DNS lookup failedhave no SHOW-command counter, so pairing PgBouncer metrics with host-level log monitoring closes the error-visibility gap.
Related guides
- PgBouncer advisory locks in transaction mode: orphaned locks and mysterious contention
- PgBouncer avg_wait_time high: the latency the pool itself is injecting
- PgBouncer backend unreachable: PostgreSQL down and the pool draining
- PgBouncer capacity planning: runway for pools, clients, and PostgreSQL slots
- PgBouncer client connection leak: idle clients that never disconnect
- How PgBouncer actually works in production: a mental model for operators
- PgBouncer LISTEN/NOTIFY not working: why pub/sub needs session pooling
- PgBouncer max_client_conn tuning: setting the client limit against real FD headroom
- PgBouncer maxwait high: the oldest client waiter and how close it is to timing out
- PgBouncer monitoring checklist: the signals every connection pooler needs
- PgBouncer monitoring maturity model: from survival to expert
- PgBouncer no more connections allowed (max_client_conn): the front door is full






