How to Perform a Hot Standby in PostgreSQL

How to Perform a Hot Standby in PostgreSQL

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:

  1. 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.
  2. 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:

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:

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:

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

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

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.

Exit mobile version