If you’ve been running PostgreSQL in production for any length of time, you’ve probably lost sleep over one question: what happens if my primary server goes down? I’ve been there — staring at a dashboard at 2 AM, watching connection errors pile up, wishing I had a warm (or hot) copy of the database ready to take over. That’s exactly what a hot standby solves, and in this guide I’m going to walk you through what it is, how it works, and how to set one up yourself, step by step.
What Is a Hot Standby in PostgreSQL?
A hot standby is a replica of your PostgreSQL database that stays in a constant state of recovery, continuously applying the write-ahead log (WAL) records streamed from a primary server. Unlike a cold standby (which is just a copy of your data files sitting there doing nothing) or a warm standby (which is receiving WAL but can’t be queried), a hot standby server can accept read-only queries while it’s replaying changes from the primary.
This gives you two big benefits at once:
- High availability — if the primary fails, the standby has an up-to-date (or nearly up-to-date) copy of the data and can be promoted to become the new primary.
- Read scaling — you can offload reporting queries, analytics, and other read-heavy workloads to the standby, reducing load on your primary.
Hot standby was introduced back in PostgreSQL 9.0, and it’s built on top of two other core features: Write-Ahead Logging (WAL) and Streaming Replication. Understanding hot standby really means understanding how these three pieces fit together.
How Hot Standby Actually Works
Every change made to a PostgreSQL database is first written to the WAL before it touches the actual data files. This is what makes crash recovery possible in the first place — if the server crashes, it can replay the WAL to get back to a consistent state.
Streaming replication takes this a step further: instead of just using WAL for local crash recovery, PostgreSQL can ship those WAL records over the network to one or more standby servers in near real-time. The standby server sits in continuous recovery mode, constantly replaying the incoming WAL records against its own copy of the data files.
Hot standby is the setting that allows that standby server, while it’s replaying WAL, to also serve read-only client connections. Without hot standby enabled, a standby server in recovery mode would reject all client connections until it’s promoted.
Prerequisites
Before you start, make sure you have:
- Two (or more) servers running the same major version of PostgreSQL. Mixing versions between primary and standby is not supported.
- Network connectivity between the primary and standby on the PostgreSQL port (default 5432).
- A dedicated replication user with the
REPLICATIONprivilege. - Enough disk space on the standby to hold a full copy of the primary’s data directory.
- SSH access (or another secure method) to transfer the base backup, unless you’re using a tool like
pg_basebackupover the network directly.
I’ll assume you already have streaming replication configured, since hot standby is really just the “can I query this replica” layer on top of it. If you haven’t set up replication yet, I’ve written a separate, dedicated guide called “How to Set Up Replication in PostgreSQL” that walks through the base backup and primary_conninfo configuration in detail — it’s worth reading first if you’re starting completely from scratch.
Step 1: Configure the Primary Server
On the primary, open postgresql.conf and make sure these settings are in place:
wal_level = replica
max_wal_senders = 10
wal_keep_size = 1024
hot_standby = on
A quick note on each:
wal_level = replicatells PostgreSQL to include enough information in the WAL to support replication (as opposed tominimal, which only supports crash recovery).max_wal_senderscontrols how many concurrent connections can be used for streaming WAL. Set it a bit higher than the number of standbys you plan to run.wal_keep_size(orwal_keep_segmentson older versions) determines how much WAL is retained on the primary in case a standby falls behind, so it doesn’t need a full base backup every time there’s a hiccup.hot_standby = onneeds to be set on the primary too, since it gets inherited by any standby created from a base backup of this server.
You’ll also need an entry in pg_hba.conf allowing the replication user to connect:
host replication replicator 192.168.1.20/32 scram-sha-256
Replace the IP with your actual standby server’s address, and create the replication role if you haven’t already:
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'strong_password_here';
Restart PostgreSQL on the primary for wal_level and max_wal_senders to take effect (these are not settings that can be reloaded on the fly).
Step 2: Take a Base Backup
On the standby server, with PostgreSQL stopped and the data directory empty, run:
pg_basebackup -h primary_host -D /var/lib/postgresql/16/main -U replicator -P -R
Let me break down these flags because they trip people up constantly:
-h primary_host— the hostname or IP of your primary server.-D— the destination data directory on the standby.-U replicator— the replication user you created earlier.-P— shows progress while the backup runs, which is genuinely useful for large databases.-R— this is the important one. It automatically writes thestandby.signalfile and populatespostgresql.auto.confwith the correctprimary_conninfosetting, saving you from doing it manually.
Depending on the size of your database, this can take anywhere from a few seconds to several hours. I always recommend running it inside a screen or tmux session for large databases so a dropped SSH connection doesn’t kill the backup halfway through.
Step 3: Enable Hot Standby on the Replica
If you used pg_basebackup -R, the standby.signal file already exists in the data directory, which tells PostgreSQL to start in standby mode. Now open postgresql.conf on the standby and confirm:
hot_standby = on
This is usually the default value in modern PostgreSQL versions, but it’s worth double-checking explicitly, especially if you copied a config file over from somewhere else.
Step 4: Start the Standby
sudo systemctl start postgresql
Watch the logs to confirm it comes up cleanly:
tail -f /var/log/postgresql/postgresql-16-main.log
You should see something like:
LOG: entering standby mode
LOG: redo starts at 0/3000028
LOG: consistent recovery state reached at 0/30000F8
LOG: database system is ready to accept read-only connections
That last line is the one you’re looking for. It confirms hot standby is active and the server will now accept read-only queries while continuing to apply WAL from the primary.
Step 5: Verify Replication Is Working
On the primary, check pg_stat_replication:
SELECT client_addr, state, sync_state, replay_lag
FROM pg_stat_replication;
You want state to show streaming and sync_state to show either async or sync, depending on how you configured it. replay_lag tells you how far behind the standby is in real time — for most workloads this should be measured in milliseconds to a few seconds.
On the standby, you can confirm it’s in recovery mode and query-able:
SELECT pg_is_in_recovery();
This should return t (true). Try running a simple SELECT against one of your tables — it should work. Try running an INSERT — it should fail with an error along the lines of “cannot execute INSERT in a read-only transaction,” which confirms hot standby is correctly enforcing read-only access.
Common Use Cases
- Disaster recovery — promote the standby to primary if the original server fails.
- Read replicas for reporting — point your BI tools or long-running analytical queries at the standby so they don’t compete with your OLTP workload on the primary.
- Zero-downtime maintenance — you can perform certain maintenance tasks on the primary while directing read traffic to the standby temporarily.
- Geographically distributed reads — place standbys closer to users in different regions to reduce read latency.
Promoting a Standby to Primary
If your primary goes down and you need to fail over, promotion is straightforward:
pg_ctl promote -D /var/lib/postgresql/16/main
Or, from within psql:
SELECT pg_promote();
This tells the standby to stop replaying WAL and switch into a normal read-write primary. Keep in mind this is a one-way operation — once promoted, the server can’t go back to being a standby without being reconfigured as one from scratch.
Troubleshooting Common Issues
“FATAL: could not connect to the primary server” — usually a pg_hba.conf or firewall issue. Double check the standby’s IP is allowed to connect on the replication line, and that port 5432 isn’t blocked between the two hosts.
“requested WAL segment has already been removed” — this happens when the standby falls too far behind and the primary has already recycled the WAL it needs. Increase wal_keep_size on the primary, or better yet, set up a replication slot so the primary never removes WAL the standby still needs:
SELECT pg_create_physical_replication_slot('standby1_slot');
Then reference it in postgresql.auto.conf on the standby with primary_slot_name = 'standby1_slot'.
Standby accepts connections but queries hang — check for long-running transactions on the standby that are blocking WAL replay due to max_standby_streaming_delay. You may need to tune this setting depending on whether you prioritize query completion or replication currency.
Replication lag keeps growing — this is often a disk I/O bottleneck on the standby, or a network bandwidth issue between the two servers. Check iostat and network throughput before assuming it’s a PostgreSQL configuration problem.
Best Practices
- Always match PostgreSQL major versions between primary and standby.
- Use replication slots to avoid WAL removal issues, but monitor disk usage on the primary since slots prevent WAL cleanup if a standby disconnects for a long time.
- Set up monitoring on
replay_lagand alert if it exceeds a threshold that matters for your recovery point objective (RPO). - Test failover regularly. A hot standby you’ve never actually promoted in a drill is a hot standby you can’t fully trust.
- Document your promotion runbook so anyone on the team can execute a failover under pressure without guesswork.
- Consider using a connection pooler or proxy (like
pgbouncercombined with a tool such as Patroni or repmgr) if you need automated failover rather than manual promotion.
Wrapping Up
Setting up a hot standby isn’t complicated once you understand the moving parts: WAL generation on the primary, streaming replication shipping that WAL to the standby, and the hot_standby setting allowing read queries during recovery. The real value comes from testing it — don’t wait for an actual outage to find out your standby doesn’t promote cleanly. Build it, break it in a test environment, and practice the failover until it’s boring. That’s exactly how you want it to feel when it matters.