Skip to main content
RunBook Academy

PostgreSQLXV · High Availability, Failover and Disaster RecoveryHA

Client routing: DNS, VIP, proxy, service discovery

Intermediate⏱ ~30 minpsql

What you'll learn

  • Compare routing mechanisms by failover time and failure mode
  • Use target_session_attrs and state its limit precisely
  • Account for client reconnection in the real RTO
  • Avoid the routing mistakes that make a correct failover look broken

Prerequisites

Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27

Not yet marked complete on this device.

A promotion that clients cannot find is not a recovery. This is frequently the largest term in the real RTO and the least examined part of an HA design.

The mechanisms

MechanismFailover timeFails by
DNS changeTTL + cachingClients caching past the TTL
Virtual IPSecondsBoth nodes claiming it; ARP not converging
Proxy (HAProxy, pgpool)Health-check intervalBeing a single point of failure itself
target_session_attrsConnection timeOnly helping at connect
Service discoveryWatch latencyThe discovery system being down

target_session_attrs

libpq accepts a list of hosts and an attribute the chosen host must satisfy.

Read-only / Safeasking for the primary, with the standby listed first
$ psql "postgresql://postgres@rbpg-prim2,rbpg-sb:5432/postgres?target_session_attrs=read-write" \
-c "SELECT inet_server_addr(), pg_is_in_recovery()"
 172.26.0.3    in_recovery=false        <- the primary, listed second
Read-only / Safeasking for a standby, with the primary listed first
$ psql "postgresql://postgres@rbpg-sb,rbpg-prim2:5432/postgres?target_session_attrs=standby" \
-c "SELECT inet_server_addr(), pg_is_in_recovery()"
 172.26.0.2    in_recovery=true         <- the standby, listed second

libpq tried each host in order and kept the first one matching. List order did not matter; the attribute did.

The values are any (default), read-write, read-only, primary, standby and prefer-standby.

What the application must do

Routing gets a new connection to the right place. These are the application’s job:

Notice the connection is dead. Many pools discover this only on next use, so the first query after a failover fails no matter how fast the failover was. A pool with a validation query or a TCP keepalive notices sooner.

Reconnect without stampeding. Two hundred clients reconnecting simultaneously against a cluster that has just started with a cold cache is a load event on top of an incident. Jittered backoff.

Retry safely. A transaction interrupted mid-flight may or may not have committed. Retrying a non-idempotent operation blindly is how a failover produces duplicate charges. This is application design, not configuration.

Fail loudly if the new primary is not writable. A client that silently ends up on a standby and treats cannot execute INSERT in a read-only transaction as a transient error will retry forever.

# libpq keepalives, so a dead connection is noticed rather than hung on
keepalives=1 keepalives_idle=30 keepalives_interval=10 keepalives_count=3
connect_timeout=5

Those settings matter more than most of the routing discussion, because without them a broken connection can hang for the operating system’s default TCP timeout — which can be many minutes.

What to take from this

  • Four mechanisms, four failover times, four failure modes.
  • target_session_attrs picks a host by role at connect time, and list order does not matter — measured both ways after a real failover.
  • It does nothing for connections already open. Those break, and the application’s reconnect behaviour is your real RTO.
  • Set libpq keepalives and connect_timeout, or a broken connection can hang for minutes.
  • DNS TTL is a lower bound. Measure what your stack actually does.
  • A proxy should ask the HA stack who the primary is, not ask PostgreSQL. Fence, promote, then route.

Cross-course references

  • Linux for Production Sysadmins — Part XXIII (DNS) covers TTL and negative caching, which decide how long clients keep using a dead primary, and Part LVI (Keepalived and VRRP) covers the VIP alternative.
  • Kubernetes for Production Sysadmins — Part XXXVIII (Services), Part XXXIX (Service discovery) and Part XLI (CoreDNS) cover the same decision inside a cluster.
  • Observability for Production Sysadmins — Part LXIII (Synthetic monitoring) covers proving from outside that clients reach the current primary, which no database metric can tell you.

Quiz

Knowledge check · 6 questions

  1. Q1. An application uses a multi-host connection string with target_session_attrs=read-write. A failover occurs. What happens to its existing pooled connections?

  2. Q2. A proxy health-checks backends with SELECT pg_is_in_recovery() and routes writes to whichever answers false. What can go wrong during a failover?

  3. Q3. Why is a 30-second DNS TTL a lower bound on client failover time rather than a guarantee?

  4. Q4. Which are the application's responsibility rather than the routing layer's? Select all that apply.

  5. Q5. A 30-second DNS TTL guarantees that clients will reach the new primary within 30 seconds of a failover.

  6. Q6. Why is client reconnection usually the largest term in a real RTO, and what reduces it?

Passing score: 75%. Answers are checked in this browser.