Skip to content

Backup and Replication

Logical and physical backup, WAL archiving, physical and logical replication, and cluster architectures.

Backup and Replication

Logical and physical backup, WAL archiving, physical and logical replication, and cluster architectures.

This course keeps the official topic order and exposes the English handout as readable, native Markdown for review and future editorial work.

Source status: 官方 PDF 已提取为原生 Markdown

Translation baseline: English baseline. The current English archive is the working baseline; the Russian edition is retained for chapter-level cross-checking.

Course scope

Logical and physical backup, WAL archiving, physical and logical replication, and cluster architectures.

Source status

LanguageCurrent packageVersionTopicsCourse page
EnglishDBA3-handouts-16-en-20260917.zip16-202609179HTTP 404
РусскийDBA3-handouts-16.zip169HTTP 200

Contents

Supplementary materials

  • dba3_new_features.pdf
  • dba3_student_guide.pdf

Editorial status

The available English handout is extracted from the official PDF. Where no current English archive was found, the route remains ready for a later source edition.

00 Lesson 00: Backup and Replication / PostgreSQL 16

Native Markdown source page: Backup and Replication / PostgreSQL 16
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: No matching course-page link was captured in the source inventory. Archive: DBA3-handouts-16-en-20260917.zip PDF pages: 10

Page 1: Backup and Replication / PostgreSQL 16

Copyright © Postgres Professional, 2017–2026 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Alexey Beresnev Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials. Feedback Please send your feedback, comments and suggestions to: edu@postgrespro.com

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: For Administrators

Source slide page 2: For Administrators{#source-figure-002 caption=“Source slide page 2: For Administrators”}

  • DBA2, 4 days

Speaker notes

Prior knowledge of the basics of the SQL language is required to complete all courses. While we don’t provide a dedicated SQL course, there are plenty of books and resources for self-study.

For administrators, we offer the following courses.

The DBA1 basic course provides information about the PostgreSQL architecture, installation process, server configuration and management. It covers the topics of access control, backup and replication, among others.

The DBA2 course focuses on determining appropriate configuration parameters based on an understanding of the internals of the server. It also talks about monitoring the server and using feedback for iterative parameter tweaking.

The DBA3 covers backups, as well as configuration and usage scenarios of physical and logical replication. It also provides an overview of the methods and pitfalls of designing highly accessible, scalable clusters.

The DBS course focuses on client-server connections and access control features within the database system itself.

The QPT course examines the mechanisms for scheduling and executing queries, the configuration of instance parameters related to performance, and the options for finding problematic queries and optimizing them.

The PGPRO course discusses additional features provided by the Postgres Pro Enterprise DBMS.

Page 3: For Backend Developers

Source slide page 3: For Backend Developers{#source-figure-003 caption=“Source slide page 3: For Backend Developers”}

  • DEV2, 4 days

Speaker notes

For backend developers, we offer the following courses.

The basic course is the DEV1 course. It provides general information about the architecture of PostgreSQL and the use of basic database objects; server-side programming in SQL and PL/pgSQL languages using basic data types (including the composite type and arrays), as well as fronted interactions.

The DEV2 course examines the features of the server’s internal structure that affect the development of application code, and comprehensively discusses extensibility: the ability to extend server mechanisms with your own code, which allows you to use PostgreSQL to solve a wide variety of tasks.

The DBS, QPT, and PGPRO courses are intended for both developers and administrators.

Developers may also be interested in administration courses if there is a need to study the internal structure of PostgreSQL in more detail, as well as in the case when there is no dedicated administrator role on the project.

https://postgrespro.com/community/courses

Page 4: About DBA3 course

Duration: 2 days

  • Prior knowledge requirements
    • SQL fundamentals
    • proficiency in Unix operating systems
    • familiarity with PostgreSQL architecture as covered in the DBA1 course
  • What skills will be acquired
    • performing backups using various methods
    • configuring servers for physical and logical replication
    • understanding replication use cases
    • understanding approaches to building clusters

Speaker notes

This course covers backup capabilities and strategies, as well as physical and logical replication mechanisms and their use cases. It also provides an overview of the methods and pitfalls of designing highly available, scalable clusters.

Page 5: Materials Used

Pre-configured virtual machine

  • Xubuntu 24.04 OS
  • PostgreSQL 16 (three database clusters)
  • documentation in English
  • Course Handouts
    • presentations, demonstrations, practice assignments and assignment keys
    • (in html and pdf)

Speaker notes

If you are taking the course on your own, start with the Student’s guide. Among other things, it shows where to download and how to use the virtual machine

and other materials. All course materials, including the Student’s Guide, are available at: https://postgrespro.com/community/courses/DBA3

Completing practical assignments is very important to gain the basic skills in working with PostgreSQL. Be sure to try to complete the assignments yourself first, and then review the provided keys, even if the assignment did not raise questions. Assignment keys may contain additional information that is not mentioned in presentations and demonstrations. After completing a task, make sure to remove any objects you created and reset any settings you changed, as they might interfere with later tasks.

Course handouts (presentations, demonstrations, practical assignments and keys) are available in two formats. The HTML is good for browsing and copying code snippets, while pdf is paginated and convenient for printing.

Page 6: Setup and Configuration

The course uses three PostgreSQL instances: alpha, beta, and gamma:

student$ sudo pg_ctlcluster 16 alpha start
student$ sudo pg_ctlcluster 16 beta start
student$ sudo pg_ctlcluster 16 gamma start

In the demonstrations, commands on different instances have different prompts. All practice assignments are run as OS user student. A user and a database with the same name are already created in the PostgreSQL instances in order to allow you to run psql with no parameters. The server aplha runs on the standard port 5432:

student$ psql
α=> SHOW server_version;
           server_version
------------------------------------
 16.14 (Ubuntu 16.14-1.pgdg24.04+1)
(1 row)

The server beta is on 5433. Just the port is enough to connect in psql:

student$ psql -p 5433
β=> SHOW server_version;
           server_version
------------------------------------
 16.14 (Ubuntu 16.14-1.pgdg24.04+1)
(1 row)

The server gamma is on 5434.

student$ psql -p 5434
γ=> SHOW server_version;
           server_version
------------------------------------
 16.14 (Ubuntu 16.14-1.pgdg24.04+1)
(1 row)

While student is not a superuser, it can create databases and roles and can connect over the replication protocol. It also belongs to some predefined group roles:

α=> \du student
                  List of roles
 Role name |             Attributes
-----------+-------------------------------------
 student   | Create role, Create DB, Replication
α=> \drg student
                     List of role grants
 Role name |       Member of        |   Options    | Grantor
-----------+------------------------+--------------+----------
 student   | pg_checkpoint          | INHERIT, SET | postgres
 student   | pg_create_subscription | INHERIT, SET | postgres
 student   | pg_monitor             | INHERIT, SET | postgres
 student   | pg_read_server_files   | INHERIT, SET | postgres
 student   | pg_write_server_files  | INHERIT, SET | postgres
(5 rows)

In addition, student has privileges on some objects that are only available to superusers by default. Specifically, it can change parameters:

Page 7: Page 7

α=> SELECT parname, (acl).privilege_type
FROM (
  SELECT parname, aclexplode(paracl) acl
  FROM pg_parameter_acl
)
WHERE (acl).grantee = user::regrole;
           parname            | privilege_type
------------------------------+----------------
 archive_command              | ALTER SYSTEM
 archive_mode                 | ALTER SYSTEM
 archive_timeout              | ALTER SYSTEM
 checkpoint_timeout           | ALTER SYSTEM
 hot_standby_feedback         | ALTER SYSTEM
 listen_addresses             | ALTER SYSTEM
 log_recovery_conflict_waits  | ALTER SYSTEM
 max_connections              | ALTER SYSTEM
 max_slot_wal_keep_size       | ALTER SYSTEM
 max_standby_streaming_delay  | ALTER SYSTEM
 max_wal_size                 | ALTER SYSTEM
 min_wal_size                 | ALTER SYSTEM
 restore_command              | ALTER SYSTEM
 synchronous_commit           | ALTER SYSTEM
 synchronous_standby_names    | ALTER SYSTEM
 wal_keep_size                | ALTER SYSTEM
 wal_level                    | ALTER SYSTEM
 wal_receiver_status_interval | ALTER SYSTEM
(18 rows)

And can run system functions (permitted in template1 database and, as a result, in any database created with CREATE DATABASE, unless another template is specified):

α=> SELECT proname, (acl).privilege_type
FROM (
  SELECT proname, aclexplode(proacl) acl
  FROM pg_proc
)
WHERE (acl).grantee = user::regrole;
            proname             | privilege_type
--------------------------------+----------------
 pg_hba_file_rules              | EXECUTE
 pg_read_binary_file            | EXECUTE
 pg_stat_file                   | EXECUTE
 pg_stat_reset_replication_slot | EXECUTE
 pg_log_standby_snapshot        | EXECUTE
 pg_switch_wal                  | EXECUTE
 pg_wal_replay_resume           | EXECUTE
 pg_ls_dir                      | EXECUTE
 pg_read_binary_file            | EXECUTE
(9 rows)

Local connections for all users do not require a password:

α=> \c - postgres
You are now connected to database "student" as user "postgres".
α=> SELECT type, database, user_name, auth_method FROM pg_hba_file_rules();
 type  |   database    | user_name |  auth_method
-------+---------------+-----------+---------------
 local | {all}         | {all}     | trust
 host  | {all}         | {all}     | scram-sha-256
 host  | {all}         | {all}     | scram-sha-256
 local | {replication} | {all}     | trust
 host  | {replication} | {all}     | scram-sha-256
 host  | {replication} | {all}     | scram-sha-256
(6 rows)

Page 8: Course Outline and Schedule

Each training day is approximately 8 hours + lunch (1 hour)

  • Each topic usually consists of
    • presentation and demonstration: ~40-70 minutes
    • practice: ~20-40 minutes, including a break

Page 9: Day 1

Backup

    1. Logical Backup
    1. Base Backup
    1. Write-Ahead Log Archive
  • Replication
      1. Physical Replication

Page 10: Day 2

Replication (continued)

    1. Switchover to Replica 06. Logical Replication 07. Use Cases Clustering Technologies
      1. Overview

01 Lesson 01: Backup / Logical Backup

Native Markdown source page: Backup / Logical Backup
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: No matching course-page link was captured in the source inventory. Archive: DBA3-handouts-16-en-20260917.zip PDF pages: 33

Page 1: Backup / Logical Backup

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Alexey Beresnev Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • What is Logical Backup
  • Backup and Restore of Individual Tables
  • Backup and Restore of Databases
  • Backup and Restore of the Whole Cluster

Page 3: Logical Backup

SQL commands that create objects and fill them with data

    • can back up a separate object or database
    • can be restored to a different version or architecture
  • +(binary compatibility is not required)
  • − modest operation speed
  • − no point-in-time recovery

Speaker notes

A logical backup contains a set of SQL commands that can be used to restore a cluster, database, or specific tables. During the recovery process, the necessary objects are created from the logical backup and filled with data.

These commands can be run on a different server version or on a different platform or architecture (binary compatibility is not required).

In particular, a logical backup can be used for long-term storage: you can restore it even after upgrading the server to a higher version.

However, for a large database, these commands may take a while to execute. In addition to that, a logical backup can only restore your database to the point in time when the backup process was initiated.

https://postgrespro.com/docs/postgresql/16/backup-dump

Page 4: The SQL COPY command

Source slide page 4: The SQL COPY command{#source-figure-004 caption=“Source slide page 4: The SQL COPY command”}

YKS Yakutsk (129.770
MJZ Mirny (114.039
KHV Khabarovsk (1
PKC Yelizovo (158.453
UUS Khomutovo (142.718
VVO Vladivostok (1
LED Pulkovo (30.2625
KGD Khrabrovo (20.5925
KEJ Kemerovo (86.1072
CEK Chelyabinsk (61.5033
MQF Magnitogorsk (5
PEE Perm (56.0211
SGC Surgut (73.4018
  • file in the server file system owned by the PostgreSQL instance owner you can select specific columns or use any query you need new rows are added to already existing ones during recovery

Speaker notes

The COPY TO command writes the contents of a table or a query result to a file or an output stream. Rows of views or partitioned tables can be copied using a query.

COPY TO requires the pg_write_server_file role.

The COPY FROM command adds rows from a file or standard input stream to the table. It is significantly faster than similar INSERT commands. You can monitor the process in the pg_stat_progress_copy view.

COPY FROM requires the pg_read_server_files role (or read input stream pg_execute_server_program).

Note: the COPY FROM command ignores the defined rules, although integrity constraints and triggers are respected. If a table column should receive values generated by a sequence when inserted, but COPY fills that column explicitly, the sequence will not change its state (counter).

Both variants of the COPY command allow you to select the columns of the table and specify options such as:

● FORMAT – data format (text, CSV or binary);

● DELIMITER – field delimiter;

● NULL – NULL text representation;

● ENCODING – encoding;

● HEADER – header describing the columns. https://postgrespro.com/docs/postgresql/16/sql-copy

Page 5: The psql \copy Command

Source slide page 5: The psql \copy Command{#source-figure-005 caption=“Source slide page 5: The psql \copy Command”}

YKS Yakutsk (129.770
MJZ Mirny (114.039
KHV Khabarovsk (1
PKC Yelizovo (158.453
UUS Khomutovo (142.718
VVO Vladivostok (1
LED Pulkovo (30.2625
KGD Khrabrovo (20.5925
KEJ Kemerovo (86.1072
CEK Chelyabinsk (61.5033
MQF Magnitogorsk (5
PEE Perm (56.0211
SGC Surgut (73.4018
  • the file is located in the client file system and can be accessed by the OS user who has started psql the data is transferred between the client and the server the syntax and the supported features are the same as with COPY

Speaker notes

There is a\copy meta command in psql – a variant of the COPY command with the same syntax – that you can run on the client side.

The file must be available to the OS user running psql. The file is accessed on the client, and only the content is sent to the server.

https://postgrespro.com/docs/postgresql/16/app-psql#APP-PSQL-META-CO MMANDS-COPY

Page 6: The COPY command

Create a database and a table.

α=> CREATE DATABASE backup_logical;
CREATE DATABASE
α=> \c backup_logical
You are now connected to database "backup_logical" as user "student".
α=> CREATE TABLE t(
  id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  s text
);
CREATE TABLE
α=> INSERT INTO t (s) VALUES ('Hello World!'), (''), (NULL);
INSERT 0 3
α=> SELECT * FROM t;
 id |      s
----+--------------
  1 | Hello World!
  2 |
  3 |
(3 rows)

Print the table contents:

α=> COPY t TO stdout;
1
Hello World!
2
3
\N

You can see how empty rows are different from the NULL values. The output format can be configured quite flexibly. You can change the separator, the NULL display, etc. For example:

α=> COPY t TO stdout WITH (NULL '<NULL>', DELIMITER ',');
1,Hello World!
2,
3,<NULL>
Note

Note Note that the inline separator character has been escaped (the escape character is also configurable). Instead of a table, you can specify an arbitrary query.

α=> COPY (SELECT * FROM t WHERE s IS NOT NULL) TO stdout;
1
Hello World!
2

This way, you can save query results, presentation data, etc. The command supports output in CSV format, which is supported almost universally.

α=> COPY t TO stdout WITH (FORMAT csv);
1,Hello World!
2,""
3,

Input from file or console works in a similar manner.

α=> TRUNCATE TABLE t;
TRUNCATE TABLE

However, for input from console, the end of file character (a backslash and a period) is required. In order for the data to load, you must specify the same parameters during input as were specified during output. You can also add:

Page 7: Page 7

α=> COPY t FROM stdin WHERE id != 2;
1
Привет, мир!
2
3
\N
\.
COPY 2

Here are the table contents (with NULL output configured in psql for clarity):

α=> \pset null '\\N'
Null display is "\N".
α=> SELECT * FROM t;
 id |      s
----+--------------
  1 | Привет, мир!
  3 | \N
(2 rows)

Load all the rows:

α=> TRUNCATE TABLE t;
TRUNCATE TABLE
α=> COPY t FROM stdin;
1
Привет, мир!
2
3
\N
\.
COPY 3
α=> SELECT * FROM t;
 id |      s
----+--------------
  1 | Привет, мир!
  2 |
  3 | \N
(3 rows)

Page 8: Database Backup

Source slide page 8: Database Backup{#source-figure-008 caption=“Source slide page 8: Database Backup”}

--
-- PostgreSQL database dum
--
-- Dumped from database ve
-- Dumped by pg_dump versi
SET statement_timeout = 0;
SET lock_timeout = 0;
SET
idle_in_transaction_sessio
SET client_encoding = 'UTF
SET standard_conforming_st
  • format: SQL commands you can specify the database objects to be dumped the new database must be created from the template0 template roles and tablespaces must be created in advance you should usually run ANALYZE after recovery

Speaker notes

The pg_dump utility creates a full-scale database backup.

If you do not specify a file name (-f, –file), the utility will display the result to the standard output stream.

The produced output is a script to be run in psql; it contains the commands that will create the objects and fill them with data.

Additional keys can limit the set of objects: select specified tables or all objects in specified schemas, upload only object definitions or only data, etc.

Note that the database to be restored should be cloned from template0 since all the changes made in template1 will also make it into the backup.

Besides, all the required roles and tablespaces must be set up in advance. Since these objects do not belong to any particular database, they won’t be included into the dump.

Once the database is restored, it is a good idea to run the ANALYZE command: it will collect statistics that the optimizer requires for query planning.

https://postgrespro.com/docs/postgresql/16/app-pgdump

Page 9: pg_dumpall Utility

pg_dump, ran without additional parameters, issues SQL commands that create all objects in the current database: To get commands to recover to a system with a different set of roles, use the options: -O, –no-owner — do not generate commands to set the owner of objects; -x, –no-acl — do not generate commands to set privileges

student$ pg_dump -d backup_logical --no-acl
--
-- PostgreSQL database dump
--
\restrict gJnV8i7WLYQP14BMyhefNauB3FcoS7S01BBdaZbOzlEf6c8cdx6vlOAs0g7GdGX
-- Dumped from database version 16.14 (Ubuntu 16.14-1.pgdg24.04+1)
-- Dumped by pg_dump version 16.14 (Ubuntu 16.14-1.pgdg24.04+1)
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- Name: t; Type: TABLE; Schema: public; Owner: student
--
CREATE TABLE public.t (
    id integer NOT NULL,
    s text
);
ALTER TABLE public.t OWNER TO student;
--
-- Name: t_id_seq; Type: SEQUENCE; Schema: public; Owner: student
--
ALTER TABLE public.t ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
    SEQUENCE NAME public.t_id_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1
);
--
-- Data for Name: t; Type: TABLE DATA; Schema: public; Owner: student
--
COPY public.t (id, s) FROM stdin;
1
Привет, мир!
2
3
\N
\.
--
-- Name: t_id_seq; Type: SEQUENCE SET; Schema: public; Owner: student
--

Page 10: Page 10

SELECT pg_catalog.setval('public.t_id_seq', 3, true);
--
-- Name: t t_pkey; Type: CONSTRAINT; Schema: public; Owner: student
--
ALTER TABLE ONLY public.t
    ADD CONSTRAINT t_pkey PRIMARY KEY (id);
--
-- PostgreSQL database dump complete
--
\unrestrict gJnV8i7WLYQP14BMyhefNauB3FcoS7S01BBdaZbOzlEf6c8cdx6vlOAs0g7GdGX

Here, pg_dump has created the table t and populated it using the COPY command from before. The \--column-inserts\\ flag enables the use of INSERT commands, but the process will take significantly longer. Logical backups in plain format may include malicious metacommands, for example, \gexec. To avoid such issues when running the script in psql, pg_dump prepends it with the \restrict metacommand with the key: This command switches on the restricted mode, making psql ignore all metacommands except for \unrestrict with the same key. The keys are handy for dumping and uploading data in segments: -s, –schema-only — back up only object definitions without data -a, –data-only — back up only data without creating objects For recovering to a system already containing some data (or, on the other hand, to a completely clean system): -c, –clean — generate DROP commands for created objects -C, –create — generate commands for creating a database and connecting to it

Note

Important: changes to the template1 database are dumped too. Therefore, you should recover to a database created from ’template0’. The \’–create\’ flag takes it into account automatically:

student$ pg_dump --create -d backup_logical | grep 'CREATE DATABASE'
CREATE DATABASE backup_logical WITH TEMPLATE = template0 ENCODING = 'UTF8'
LOCALE_PROVIDER = libc LOCALE = 'en_US.UTF-8';

Some flags specify the objects to be dumped: -n, –schema — template for schema names -t, –table — template for table names Or, conversely, not to be, while dumping everything else: -N, –exclude-schema — template for schema names -T, –exclude-table — template for table names For example, recover the table ’t’ to another database on another server.

student$ psql -p 5433
β=> CREATE DATABASE backup_logical_1;
CREATE DATABASE
student$ pg_dump --table=t -d backup_logical | psql -p 5433 -d backup_logical_1
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
SET
CREATE TABLE

Page 11: Page 11

ALTER TABLE
ALTER TABLE
COPY 3
 setval
--------
      3
(1 row)
ALTER TABLE
β=> \c backup_logical_1
You are now connected to database "backup_logical_1" as user "student".
β=> SELECT * FROM t;
 id |      s
----+--------------
  1 | Привет, мир!
  2 |
  3 |
(3 rows)

Page 12: The Custom Format

Source slide page 12: The Custom Format{#source-figure-012 caption=“Source slide page 12: The Custom Format”}

;
; Archive created at 2017-
;     dbname: demo
;     TOC Entries: 146
;     Compression: -1
;     Dump Version: 1.12-0
;     Format: CUSTOM
;
; Selected TOC Entries:
;
2297; 1262 475453 DATABASE
5; 2615 475454 SCHEMA - bo
2298; 0 0 COMMENT - SCHEMA
  • an internal format with a table of contents database objects to be restored can be selected at the time of recovery recovery can be performed in parallel mode

Speaker notes

The pg_dump utility supports several backup formats. By default, the plain format is used; it provides pure psql commands.

The custom (-F c, –format = custom) format creates a backup in a special format containing not only objects, but also a table of contents. Having a TOC allows you to choose the objects to be restored right at the time of recovery, not while making the dump. By default, the the custom format output is compressed.

To restore the database, you need to run another utility: pg_restore. It reads the file and converts it to psql commands. If you do not explicitly provide the database name (in the -d option), all commands will be output to the standard output stream. If you do specify a database name, the utility will connect to that database and execute the commands directly, without using psql.

To restore only some of the objects, you can use one of the following approaches. The first one is to filter the objects to be restored, just like it is done in pg_dump. pg_restore actually supports many of the same options as pg_dump.

The second option is to use the TOC to retrieve the list of objects included into the backup (via the –list option). Then you can edit this list manually: delete the objects you do not need and pass the modified list to pg_restore (via the –use-list option).

Uploading can be performed in parallel by specifying the number of threads with the -j switch – this is another advantage of the custom format.

https://postgrespro.com/docs/postgresql/16/app-pgdump

Page 13: pg_dump: the \’custom\’ format

Source slide page 13: pg_dump: the \’custom\’ format{#source-figure-013 caption=“Source slide page 13: pg_dump: the \’custom\’ format”}

The ’plain’ format has a serious limitation: you have to specify all the objects you will need in your backup before making the dump. The ’custom’ format allows you to make a full dump first, then specify which objects to recover from it later.

student$ pg_dump --format=custom -d backup_logical -f /home/student/tmp/backup_logical.custom

This is done with the pg_resore tool. Time to recover ’t’ again.

β=> DROP TABLE t;
DROP TABLE
student$ pg_restore --table=t -p 5433 -d backup_logical_1 --no-acl
/home/student/tmp/backup_logical.custom

pg_restore will recognize the dump format automatically, there is no need to specify it. pg_restore also understands all the flags that pg_dump does, and more: -I, –index — load specific indexes -P, –function — load certain functions -T, –trigger — load specific triggers. Let’s check how the table recovered:

β=> SELECT * FROM t;
 id |      s
----+--------------
  1 | Привет, мир!
  2 |
  3 |
(3 rows)

Another example: restore the entire original backup_logical database to another server.

student$ pg_restore --create -p 5433 -d postgres --no-acl /home/student/tmp/backup_logical.custom

Here, we have specified the postgres database. If another database was specified, pg_restore would have created the necessary database and immediately switched to it. Check:

β=> \c backup_logical
You are now connected to database "backup_logical" as user "student".
β=> SELECT * FROM t;
 id |      s
----+--------------
  1 | Привет, мир!
  2 |
  3 |
(3 rows)

Page 14: The Directory Format

Source slide page 14: The Directory Format{#source-figure-014 caption=“Source slide page 14: The Directory Format”}

the directory contains a a TOC file and a separate file for each database object database objects to be restored can be selected at the time of recovery both dump and restore operations can be performed in the parallel mode

Speaker notes

Another backup format is Directry. Using this format, pg_dump produces a whole directory instead of a single file; it contains the backed up objects and the table of contents. By default, all files in the directory are compressed.

Its advantage over the custom format is that such a backup can be created concurrently using several processes (the number of processes is specified in option -j, –jobs).

Naturally, the backup will contain consistent data even though it has been created concurrently. Consistency is ensured by using a single data snapshot for all parallel processes.

https://postgrespro.com/docs/postgresql/16/functions-admin#FUNCTIONS-S NAPSHOT-SYNCHRONIZATION

Recovery, as with the custom format, supports parallel streaming.

Otherwise, working with the directory format offers the same capabilities as the formats discussed earlier: it supports the same options and approaches.

Page 15: pg_dump: the ’directory’ format

The \’direcotry\’ format, notably, supports dumping in parallel.

student$ pg_dump --format=directory \
  --jobs=2 \
  -d backup_logical \
  -f /home/student/tmp/backup_logical.directory

Consistency is guaranteed: all processes use the same snapshot. Take a look inside the directory:

student$ ls -l /home/student/tmp/backup_logical.directory
total 12
-rw-rw-r-- 1 student student   60 Sep 16 23:52 3440.dat.gz
-rw-rw-r-- 1 student student 4985 Sep 16 23:52 toc.dat

There is a table of contents file, and a file for each dumped object (we have only one):

student$ zcat /home/student/tmp/backup_logical.directory/3440.dat.gz
1
Привет, мир!
2
3
\N
\.

To restore from a backup, first disconnect from the backup_logical database:

β=> \q

The --clean key generates a command to delete the existing database:

student$ pg_restore --clean \
  --create \
  --jobs=2 \
  -p 5433 \
  -d postgres /home/student/tmp/backup_logical.directory \
  --no-acl
student$ psql --pset null='\N' -p 5433 -d backup_logical -c 'SELECT * FROM t'
 id |      s
----+--------------
  1 | Привет, мир!
  2 |
  3 | \N
(3 rows)

Page 16: Format Comparison

plain custom directory tar

  • utility for of recovery psql pg_restore
  • compression gzip, lz4, or zstd
  • partial restore possible possible possible
  • parallel backup possible
  • parallel restore possible possible

Speaker notes

This slide compares different features provided by different backup formats.

There is also a fourth format – tar. It was not discussed here, since it does not introduce anything new and does not provide advantages over other formats. In fact, this format is simply a tar-ed version of the directory format, but it does not support compression or parallel execution.

Page 17: Cluster Backup

Source slide page 17: Cluster Backup{#source-figure-017 caption=“Source slide page 17: Cluster Backup”}

--
-- PostgreSQL database clu
--
SET default_transaction_re
SET client_encoding = 'UTF
SET standard_conforming_st
--
-- Roles
--
CREATE ROLE postgres;
  • format: SQL commands dumps the whole cluster, including roles and tablespaces the user must have access to all objects in the database cluster parallel backups are not supported

Speaker notes

pg_dumpall creates a backup of the entire cluster, including roles and tablespaces.

Since pg_dumpall requires access to all objects of all databases, it should be ran by the superuser. pg_dumpall connects to each database in the cluster one by one and backups them using pg_dump. In addition, it also stores data related to the cluster as a whole.

To start this process, pg_dumpall has to establish a connection with a database. By default, postgres or template1 is selected, but you can specify another one.

The result of pg_dumpall is a script for psql. Other formats are not supported. This means that pg_dumpall does not support parallel execution, which can be a problem for larger clusters. In this case, you can use the – globals-only key to backup only roles and table spaces, and unload the databases themselves separately using parallel pg_dump.

https://postgrespro.com/docs/postgresql/16/app-pg-dumpall

Page 18: The pg_dumpall Utility

The pg_dump utility is good for dumping a single database, but it never dumps shared database cluster objects such as roles and tablespaces. To create a full cluster backup, you need to use pg_dumpall. The pg_dumpall, pg_dump, and pg_restore utilities do not require any separate privileges, but the role performing them must have privileges to read (create) all affected objects. For example, pg_dump can be used by the database owner. But since it is necessary to have access to all databases to copy the cluster, we run pg_dumpall as the superuser.

student$ pg_dumpall --clean -U postgres -f /home/student/tmp/alpha.sql

The backup includes: commands that the pg_dump backs up for each database commands for shared cluster objects (roles and tablespaces) Commands for shared objects can be obtained separately:

student$ pg_dumpall --clean --globals-only -U postgres
--
-- PostgreSQL database cluster dump
--
\restrict e0B6e88kQ1UH20c5WvNCp9aKNhv1EmAjMAub1ie8JhGQUF6v4A2nXO4FH1ZQzZa
SET default_transaction_read_only = off;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
--
-- Drop roles
--
DROP ROLE postgres;
DROP ROLE student;
--
-- Roles
--
CREATE ROLE postgres;
ALTER ROLE postgres WITH SUPERUSER INHERIT CREATEROLE CREATEDB LOGIN REPLICATION
BYPASSRLS PASSWORD
'SCRAM-SHA-256$4096:AsXBHJhp+lTo5cMW8rTWVA==$w9/d0pa/3sFrh+sLgBYnALVW+k7RETzghidz4iOF850=:
A0khfLkZ1ZZrQUD39l/Uiro+tu/0FoROEOk9UNyzqx0=';
CREATE ROLE student;
ALTER ROLE student WITH NOSUPERUSER INHERIT CREATEROLE CREATEDB LOGIN REPLICATION
NOBYPASSRLS PASSWORD
'SCRAM-SHA-256$4096:79AZA/AciQ7/18/dXmAZZQ==$iy9RopOHolUKGAK/JK2K/pUdhj2GMsZLTgbWc8vH6sc=:
ZiyLRbFp1NxhgDuzBGithNum7+5zdbGG8XeAZJ6MR7A=';
--
-- User Configurations
--
--
-- Role memberships
--
GRANT pg_checkpoint TO student WITH INHERIT TRUE GRANTED BY postgres;
GRANT pg_create_subscription TO student WITH INHERIT TRUE GRANTED BY postgres;
GRANT pg_monitor TO student WITH INHERIT TRUE GRANTED BY postgres;
GRANT pg_read_server_files TO student WITH INHERIT TRUE GRANTED BY postgres;
GRANT pg_write_server_files TO student WITH INHERIT TRUE GRANTED BY postgres;
--
-- Role privileges on configuration parameters
--

Page 19: Page 19

GRANT ALTER SYSTEM ON PARAMETER archive_command TO student;
GRANT ALTER SYSTEM ON PARAMETER archive_mode TO student;
GRANT ALTER SYSTEM ON PARAMETER archive_timeout TO student;
GRANT ALTER SYSTEM ON PARAMETER checkpoint_timeout TO student;
GRANT ALTER SYSTEM ON PARAMETER hot_standby_feedback TO student;
GRANT ALTER SYSTEM ON PARAMETER listen_addresses TO student;
GRANT ALTER SYSTEM ON PARAMETER log_recovery_conflict_waits TO student;
GRANT ALTER SYSTEM ON PARAMETER max_connections TO student;
GRANT ALTER SYSTEM ON PARAMETER max_slot_wal_keep_size TO student;
GRANT ALTER SYSTEM ON PARAMETER max_standby_streaming_delay TO student;
GRANT ALTER SYSTEM ON PARAMETER max_wal_size TO student;
GRANT ALTER SYSTEM ON PARAMETER min_wal_size TO student;
GRANT ALTER SYSTEM ON PARAMETER restore_command TO student;
GRANT ALTER SYSTEM ON PARAMETER synchronous_commit TO student;
GRANT ALTER SYSTEM ON PARAMETER synchronous_standby_names TO student;
GRANT ALTER SYSTEM ON PARAMETER wal_keep_size TO student;
GRANT ALTER SYSTEM ON PARAMETER wal_level TO student;
GRANT ALTER SYSTEM ON PARAMETER wal_receiver_status_interval TO student;
\unrestrict e0B6e88kQ1UH20c5WvNCp9aKNhv1EmAjMAub1ie8JhGQUF6v4A2nXO4FH1ZQzZa
--
-- PostgreSQL database cluster dump complete
--

Recovery is done via psql, there is no other way around it.

student$ psql -p 5433 -U postgres -f /home/student/tmp/alpha.sql
SET
SET
SET
DROP DATABASE
DROP DATABASE
psql:/home/student/tmp/alpha.sql:26: ERROR:  current user cannot be dropped
psql:/home/student/tmp/alpha.sql:27: ERROR:  role "student" cannot be dropped because
some objects depend on it
DETAIL:  owner of database backup_logical_1
1 object in database backup_logical_1
psql:/home/student/tmp/alpha.sql:34: ERROR:  role "postgres" already exists
ALTER ROLE
psql:/home/student/tmp/alpha.sql:36: ERROR:  role "student" already exists
ALTER ROLE
psql:/home/student/tmp/alpha.sql:48: NOTICE:  role "student" has already been granted
membership in role "pg_checkpoint" by role "postgres"
GRANT ROLE
psql:/home/student/tmp/alpha.sql:49: NOTICE:  role "student" has already been granted
membership in role "pg_create_subscription" by role "postgres"
GRANT ROLE
psql:/home/student/tmp/alpha.sql:50: NOTICE:  role "student" has already been granted
membership in role "pg_monitor" by role "postgres"
GRANT ROLE
psql:/home/student/tmp/alpha.sql:51: NOTICE:  role "student" has already been granted
membership in role "pg_read_server_files" by role "postgres"
GRANT ROLE
psql:/home/student/tmp/alpha.sql:52: NOTICE:  role "student" has already been granted
membership in role "pg_write_server_files" by role "postgres"
GRANT ROLE
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
SET

Page 20: Page 20

SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
UPDATE 1
DROP DATABASE
CREATE DATABASE
ALTER DATABASE
You are now connected to database "template1" as user "postgres".
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
COMMENT
ALTER DATABASE
You are now connected to database "template1" as user "postgres".
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
REVOKE
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
CREATE DATABASE
ALTER DATABASE
You are now connected to database "backup_logical" as user "postgres".
SET
SET
SET

Page 21: Page 21

SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
SET
CREATE TABLE
ALTER TABLE
ALTER TABLE
COPY 3
 setval
--------
      3
(1 row)
ALTER TABLE
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
DROP DATABASE
CREATE DATABASE
ALTER DATABASE
You are now connected to database "postgres" as user "postgres".
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
COMMENT
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
CREATE DATABASE

Page 22: Page 22

ALTER DATABASE
You are now connected to database "student" as user "postgres".
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT

During recovery, errors related to irremovable objects or objects already existing may come up. These are normal and do not affect recovery in this case.

student$ psql -p 5433
β=> SELECT datname FROM pg_database;
     datname
------------------
 template0
 backup_logical_1
 template1
 backup_logical
 postgres
 student
(6 rows)
β=> \c backup_logical
You are now connected to database "backup_logical" as user "student".
β=> SELECT * FROM t;
 id |      s
----+--------------
  1 | Привет, мир!
  2 |
  3 |
(3 rows)

The impact of row level security

If some tables have row level security on (more on that in the Row Level Security topic in the DBS course), you could end up with incomplete data and not even notice. To avoid that, before running COPY, set row\_security to off. This way, when a row level security policy triggers, the process will throw an explicit error. A simple example. Set up a policy that prohibits displaying empty rows, then enable it for the table owner.

α=> CREATE POLICY t_s_not_null ON t USING (s IS NOT NULL);
CREATE POLICY
α=> ALTER TABLE t ENABLE ROW LEVEL SECURITY;
ALTER TABLE
α=> ALTER TABLE t FORCE ROW LEVEL SECURITY;
ALTER TABLE

Now COPY returns just two rows:

α=> COPY t TO stdout;

Page 23: Page 23

1
Привет, мир!
2

But when the parameter is set to off, we get an error:

α=> SET row_security = off;
SET
α=> COPY t TO stdout;
ERROR:  query would be affected by row-level security policy for table "t"
HINT:  To disable the policy for the table's owner, use ALTER TABLE NO FORCE ROW LEVEL
SECURITY.

Both pg_dump and pg_dumpall automatically set row\_security = off, no additional actions needed:

student$ pg_dump -d backup_logical -t t > /dev/null
pg_dump: error: query failed: ERROR:  query would be affected by row-level security
policy for table "t"
HINT:  To disable the policy for the table's owner, use ALTER TABLE NO FORCE ROW LEVEL
SECURITY.
pg_dump: detail: Query was: COPY public.t (id, s) TO stdout;

The tool attempts to COPY the rows and returns the error: unable to copy all rows from t. If you want to copy just the rows that are available, you can use –enable-row-security:

student$ pg_dump -d backup_logical -t t --enable-row-security | grep -A3 '^COPY'
COPY public.t (id, s) FROM stdin;
1
Привет, мир!
2
\.

To bypass row level security, the user must have the BYPASSRLS attribute:

α=> \c - postgres
You are now connected to database "backup_logical" as user "postgres".
α=> ALTER USER student BYPASSRLS;
ALTER ROLE

Now, all queries by student ignore RLS policies:

student$ pg_dump -d backup_logical -t t --data-only
--
-- PostgreSQL database dump
--
\restrict wtj1ko2okrK5ZUoPNI4415UxcGzgIzm5dFN3ump9PbcyOrMHALPxQvSN500tX8d
-- Dumped from database version 16.14 (Ubuntu 16.14-1.pgdg24.04+1)
-- Dumped by pg_dump version 16.14 (Ubuntu 16.14-1.pgdg24.04+1)
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- Data for Name: t; Type: TABLE DATA; Schema: public; Owner: student
--
COPY public.t (id, s) FROM stdin;
1
Привет, мир!
2
3
\N
\.
--
-- Name: t_id_seq; Type: SEQUENCE SET; Schema: public; Owner: student
--

Page 24: Page 24

SELECT pg_catalog.setval('public.t_id_seq', 3, true);
--
-- PostgreSQL database dump complete
--
\unrestrict wtj1ko2okrK5ZUoPNI4415UxcGzgIzm5dFN3ump9PbcyOrMHALPxQvSN500tX8d

Page 25: Takeaways

You can make a logical backup of a whole cluster, a specific database or a database object

  • Good for
    • small amounts of data
    • long-term storage during which the server can been upgraded
    • migration to a different platform
  • Not so good for
    • crash recovery with minimal data loss

Page 26: Practice

1. Create two databases with tables and views, use pg\_dumpall to dump global cluster objects only, then create database backups using pg\_dump in parallel mode. Restore the entire cluster on a different server using the backups you created. 2. Create a table with two columns, one of which is the primary key containing a number sequence. Simulate a case when during uploading with the COPY command, new sequence values are not generated, leading to a unique constraint violation error when attempting to insert new rows.

Speaker notes

  1. Declare a column as GENERATED ALWAYS AS IDENTITY.

After completing the practice tasks, delete the created databases and logical backup files.

Page 27: 1. Logical Backup

Let’s create some databases, a couple of tables, and some views.

α=> CREATE DATABASE backup_logical1;
CREATE DATABASE
α=> \c backup_logical1
You are now connected to database "backup_logical1" as user "student".
α=> CREATE TABLE t1(n integer);
CREATE TABLE
α=> INSERT INTO t1 VALUES (1), (2), (3);
INSERT 0 3
α=> CREATE VIEW v1 AS SELECT * FROM t1;
CREATE VIEW
α=> CREATE DATABASE backup_logical2;
CREATE DATABASE
α=> \c backup_logical2
You are now connected to database "backup_logical2" as user "student".
α=> CREATE TABLE t2(n integer);
CREATE TABLE
α=> INSERT INTO t2 VALUES (1), (2), (3);
INSERT 0 3
α=> CREATE VIEW v2 AS SELECT * FROM t2;
CREATE VIEW

Back up the global objects.

student$ pg_dumpall --clean --globals-only -U postgres -f /home/student/tmp/alpha_globals.sql

Parallel logical database backups:

student$ pg_dump --jobs=2 \
  --format=directory \
  -d backup_logical1 \
  -f /home/student/tmp/backup_logical1.directory
student$ pg_dump --jobs=2 \
  --format=directory \
  -d backup_logical2 \
  -f /home/student/tmp/backup_logical2.directory

Now restore the cluster. Restore global objects first:

student$ psql -p 5433 -U postgres -f /home/student/tmp/alpha_globals.sql 2> /dev/null
SET
SET
SET
ALTER ROLE
ALTER ROLE
GRANT ROLE
GRANT ROLE
GRANT ROLE
GRANT ROLE
GRANT ROLE
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT

Page 28: Page 28

GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT

Then restore the two databases in parallel:

student$ pg_restore -p 5433 \
  -U postgres \
  -d postgres \
  --create \
  --jobs=2 \
  /home/student/tmp/backup_logical1.directory
student$ pg_restore -p 5433 \
  -U postgres \
  -d postgres \
  --create \
  --jobs=2 \
  /home/student/tmp/backup_logical2.directory

Check:

student$ psql -p 5433
β=> \c backup_logical1
You are now connected to database "backup_logical1" as user "student".
β=> \d
        List of relations
 Schema | Name | Type  |  Owner
--------+------+-------+---------
 public | t1   | table | student
 public | v1   | view  | student
(2 rows)
β=> \c backup_logical2
You are now connected to database "backup_logical2" as user "student".
β=> \d
        List of relations
 Schema | Name | Type  |  Owner
--------+------+-------+---------
 public | t2   | table | student
 public | v2   | view  | student
(2 rows)

Drop the databases we created.

α=> \c postgres
You are now connected to database "postgres" as user "student".
α=> DROP DATABASE backup_logical1;
DROP DATABASE
α=> DROP DATABASE backup_logical2;
DROP DATABASE

2. COPY with a Sequence-Based Table

A table with two columns:

α=> CREATE DATABASE backup_logical;
CREATE DATABASE
α=> \c backup_logical
You are now connected to database "backup_logical" as user "student".

Page 29: Page 29

α=> CREATE TABLE t(
  id int PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  txt text
);
CREATE TABLE

A table and a sequence have been created:

α=> \d
           List of relations
 Schema |   Name   |   Type   |  Owner
--------+----------+----------+---------
 public | t        | table    | student
 public | t_id_seq | sequence | student
(2 rows)

Load rows without specifying values for the first column.

α=> COPY t(txt) FROM STDIN;
Один
Два
Три
\.
COPY 3

The table contents are as expected:

α=> SELECT * FROM t;
 id | txt
----+------
  1 | Один
  2 | Два
  3 | Три
(3 rows)

The sequence counter is correct:

α=> SELECT currval('t_id_seq');
 currval
---------
       3
(1 row)

Now insert another row:

α=> INSERT INTO t(txt) VALUES('Four');
INSERT 0 1

When using INSERT, you cannot specify a value for a column declared as GENERATED ALWAYS. It must be generated by the sequence:

α=> INSERT INTO t(id,txt) VALUES(5,'Five');
ERROR:  cannot insert a non-DEFAULT value into column "id"
DETAIL:  Column "id" is an identity column defined as GENERATED ALWAYS.
HINT:  Use OVERRIDING SYSTEM VALUE to override.

However, COPY allows this:

α=> COPY t FROM STDIN;
5
Пять
6
Шесть
7
Семь
\.
COPY 3

At first glance, everything looks fine…

α=> SELECT * FROM t;

Page 30: Page 30

 id |  txt
----+-------
  1 | Один
  2 | Два
  3 | Три
  4 | Four
  5 | Пять
  6 | Шесть
  7 | Семь
(7 rows)

But it is not:

α=> INSERT INTO t(txt) VALUES('Eight');
ERROR:  duplicate key value violates unique constraint "t_pkey"
DETAIL:  Key (id)=(5) already exists.

The primary key violation occurred because COPY does not use the sequence:

α=> SELECT currval('t_id_seq');
 currval
---------
       5
(1 row)

Let’s fix this by updating the sequence state.

α=> SELECT setval('t_id_seq', max(id)) FROM t;
 setval
--------
      7
(1 row)

Now the insert will succeed:

α=> INSERT INTO t(txt) VALUES('Eight');
INSERT 0 1
α=> SELECT * FROM t;
 id |  txt
----+-------
  1 | Один
  2 | Два
  3 | Три
  4 | Four
  5 | Пять
  6 | Шесть
  7 | Семь
  8 | Eight
(8 rows)

Cleanup

Drop the database.

α=> \c postgres
You are now connected to database "postgres" as user "student".
α=> DROP DATABASE backup_logical;
DROP DATABASE

Page 31: Practice+

1. Create a table with multiple rows. Select such \\copy parameters so that when writing to a file, the correct data and NULL values are mixed up due to unsuccessful code selection. 2. Using \\copy, create a text file that denotes the missing values as N/A. Load rows from this file into a table where missing values are NULL. 3. Create a data table several MB in size and get the sizes of logical backups in the custom format with different compression methods. Also measure the time required for each compression method during the backup process.

Speaker notes

  1. If you mistakenly choose a NULL code that matches actual data values in the table columns, the resulting backup will prevent proper data restoration.

  2. A way to generate the data: SELECT (random()::numeric*(g.i))^(g.i) AS n FROM generate_series(1,2_500) AS g(i);

You can measure the time using this command: /usr/bin/time -f% e pg_dump…

After completing the practice tasks, delete the created databases and temporary files.

Page 32: 1. Breaking COPY

Create a database.

α=> CREATE DATABASE backup_logical;
CREATE DATABASE
α=> \c backup_logical
You are now connected to database "backup_logical" as user "student".

For this example, set the display of undefined values that match some data:

α=> CREATE TABLE anticopy(s text);
CREATE TABLE
α=> INSERT INTO anticopy(s) VALUES ('N'), (NULL);
INSERT 0 2

Set the null value representation to N:

α=> \copy anticopy to '/home/student/tmp/anticopy.dump' with (null 'N');
COPY 2

The output of two different values now looks identical. Properly recovering data from such a copy is impossible.

student$ cat ${HOME}/tmp/anticopy.dump
N
N

Be careful when changing the default format!

2. Loading Data with Missing Values

Create a file where missing values are marked as N/A.

α=> \copy (VALUES (0,'N/A'), (1,'One'), (2,'N/A'), (3,'Three')) to /home/student/tmp/backup_logical.txt
COPY 4
α=> \! cat /home/student/tmp/backup_logical.txt
0
N/A
1
One
2
N/A
3
Three

Create a table to load the data into.

α=> CREATE TABLE t (k int, v text);
CREATE TABLE

Load the data from the file into the table. The N/A value should be loaded as NULL.

α=> \copy t from /home/student/tmp/backup_logical.txt null 'N/A'
COPY 4
α=> SELECT *, v IS NULL "IsNull" FROM t;
 k |   v   | IsNull
---+-------+--------
 0 |       | t
 1 | One   | f
 2 |       | t
 3 | Three | f
(4 rows)

3. Compression in pg\_dump

Create a large enough table.

Page 33: Page 33

α=> CREATE TABLE mb AS
  SELECT (random()::numeric*(g.i))^(g.i) AS n
  FROM generate_series(1,2_500) AS g(i);
SELECT 2500
α=> SELECT pg_size_pretty(pg_table_size('mb'));
 pg_size_pretty
----------------
 5128 kB
(1 row)

Measure the time it takes to dump data without compression (and warm up the cache in the process).

student$ /usr/bin/time -f %e pg_dump \
  --format=custom --dbname=backup_logical \
  --compress=none --file=/home/student/tmp/backup_logical.none
0.45

How long will the dump take with gzip compression (used by default)?

student$ /usr/bin/time -f %e pg_dump \
  --format=custom --dbname=backup_logical \
  --compress=gzip --file=/home/student/tmp/backup_logical.gzip
2.01

Now try lz4.

student$ /usr/bin/time -f %e pg_dump \
  --format=custom --dbname=backup_logical \
  --compress=lz4 --file=/home/student/tmp/backup_logical.lz4
0.33

Now zstd.

student$ /usr/bin/time -f %e pg_dump \
  --format=custom --dbname=backup_logical \
  --compress=zstd --file=/home/student/tmp/backup_logical.zstd
0.64

Compare the file sizes:

student$ ls -lhS ~/tmp/backup_logical.*
-rw-rw-r-- 1 student student 8.3M Sep 17 00:06 /home/student/tmp/backup_logical.none
-rw-rw-r-- 1 student student 7.9M Sep 17 00:06 /home/student/tmp/backup_logical.lz4
-rw-rw-r-- 1 student student 4.1M Sep 17 00:06 /home/student/tmp/backup_logical.zstd
-rw-rw-r-- 1 student student 3.9M Sep 17 00:06 /home/student/tmp/backup_logical.gzip
-rw-rw-r-- 1 student student   26 Sep 17 00:06 /home/student/tmp/backup_logical.txt

Conclusions: gzip has the best compression ratio, but takes several times longer to run lz4 is the fastest, but offers very little compression benefit zstd strikes a good balance: its compression ratio is only slightly behind gzip, while its speed is comparable to lz4

Cleanup

Delete the databases and temporary files we created.

student$ rm ~/tmp/backup_logical.*
student$ rm ~/tmp/anticopy.dump
α=> \c postgres
You are now connected to database "postgres" as user "student".
α=> DROP DATABASE backup_logical;
DROP DATABASE

02 Lesson 02: Backup / Base Backup

Native Markdown source page: Backup / Base Backup
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: No matching course-page link was captured in the source inventory. Archive: DBA3-handouts-16-en-20260917.zip PDF pages: 26

Page 1: Backup / Base Backup

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Alexey Beresnev Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • What is Physical Backup
  • Cold and Рot Backup
  • Replication Protocol and Replication Slots
  • Cluster Cloning and Backup
  • Integrity Verification
  • Backup API

Page 3: Physical Backup

Physical base copy – copy of all cluster files

    • faster than logical backup
    • restores statistics
  • can restore only to compatible systems
  • − and major PostgreSQL versions
  • − whole cluster backup only, no partial backups

Speaker notes

Physical backup involves copying all files related to the database cluster (both data and service files), that is, creating a full binary copy.

Copying files is faster than dumping all SQL commands, as you do for a logical backup. Likewise, starting a server from a physical backup is a matter of minutes, while recovery from a logical backup takes much longer. Statistical data is also restored from the physical copy, so you don’t need to rebuild it.

But there are shortcomings. A physical backup can be used to restore the system only on a compatible platform (that has the same OS, architecture, etc.) with the same PostgreSQL major version installed. Additionally, you cannot create a physical backup of individual databases within a cluster – only the entire cluster can be backed up.

Page 4: Cold Backup

Server is off

  • if the checkpoint is completed, you can just copy the files
  • the backup can be deployed on a different server, regardless of the PGDATA
  • location
    • simplicity
  • − requires service interruption
  • File system snapshot
    • Similar to an unexpected shutdown: recovery required on startup, but the
    • backup has all necessary WAL files
    • all data, including tablespaces, must be captured in a single snapshot
      • no server downtime required
    • − file systems with snapshot support are generally slower

Speaker notes

A cold backup is made when the data on disk is in a consistent state: that is, all pages are stored on disk and are up to date. Restoring from such a backup is straightforward: just extract the files, start the server, and it’s good to go.

But to create such a backup, you must perform a checkpoint and a clean server shutdown first. This is a major drawback: shutdowns can take a while, especially with large volumes of data.

The downtime can be reduced by running rsync (or a similar tool) beforehand while the server is still running. After the server is shut down, rsync will only need to copy the changes (which should be relatively few). However, some downtime is still unavoidable.

Another option is to make an inconsistent copy, which will include pages saved at different times. This inconsistency is inevitable when the server is shut down incorrectly or when creating a snapshot of the file system. In the latter case, all cluster files should fall into one snapshot. This may not be possible if the files are on different file systems.

When recovering from inconsistent data, the server will need to restore consistency at startup. This is the standard automatic crash recovery procedure. This isn’t a problem since the necessary WAL files are guaranteed to be backed up, though the recovery process can take some time.

https://postgrespro.com/docs/postgresql/16/backup-file

Page 5: Cold File-Based Backup

You can copy the files of a stopped cluster and start a second server with them. It doesn’t matter whether the server was shut down cleanly or not. Create a database and a table.

student$ psql
α=> CREATE DATABASE backup_base;
CREATE DATABASE
α=> \c backup_base
You are now connected to database "backup_base" as user "student".
α=> CREATE TABLE t(s text);
CREATE TABLE
α=> INSERT INTO t VALUES ('This row was inserted on α...');
INSERT 0 1

Crash the server and copy the files to beta:

student$ sudo kill -QUIT $(sudo head -n 1 /var/lib/postgresql/16/alpha/postmaster.pid)
student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
student$ sudo rm -rf /var/lib/postgresql/16/beta
student$ sudo cp -a /var/lib/postgresql/16/alpha /var/lib/postgresql/16/beta

Beta starts and restores consistency:

student$ sudo pg_ctlcluster 16 beta start
student$ sudo tail -n 7 /var/log/postgresql/postgresql-16-beta.log
2026-09-16 23:54:12.254 MSK [70228] LOG:  database system was not properly shut down;
automatic recovery in progress
2026-09-16 23:54:12.272 MSK [70228] LOG:  redo starts at 0/1B82F10
2026-09-16 23:54:12.344 MSK [70228] LOG:  invalid record length at 0/1FB5138: expected at
least 24, got 0
2026-09-16 23:54:12.344 MSK [70228] LOG:  redo done at 0/1FB5110 system usage: CPU: user:
0.00 s, system: 0.00 s, elapsed: 0.07 s
2026-09-16 23:54:12.483 MSK [70226] LOG:  checkpoint starting: end-of-recovery immediate
wait
2026-09-16 23:54:15.888 MSK [70226] LOG:  checkpoint complete: wrote 932 buffers (5.7%);
0 WAL file(s) added, 0 removed, 0 recycled; write=0.041 s, sync=3.293 s, total=3.421 s;
sync files=308, longest=0.026 s, average=0.011 s; distance=4296 kB, estimate=4296 kB;
lsn=0/1FB5138, redo lsn=0/1FB5138
2026-09-16 23:54:15.913 MSK [70225] LOG:  database system is ready to accept connections
student$ psql -p 5433 -d backup_base
β=> SELECT * FROM t;
               s
-------------------------------
 This row was inserted on α...
(1 row)

Page 6: Hot Backup

Server is running

  • pages are not consistent, as in case of incorrect shutdown
  • pages may change while being copied
  • (the non-atomic read and and write problem)
  • restoring consistency requires all WAL records
  • from the last checkpoint till the backup completion
  • (some files may get deleted during backup)
    • no service interruption
  • − specialized tools required

Speaker notes

Hot backup is performed on a running server, so it will probably contain inconsistent pages.

In addition to that, during backup, data is read directly from files rather than through the buffer cache. The contents of the files on disk obviously may change while the backup process is running, and there may not be a guarantee of the atomicity of reading/writing PostgreSQL pages (this depends on the file system and hardware. See “WAL Configuration” section of the DBA2 course for more). As a result, the backup will contain corrupted pages that can’t be recovered even using WAL. This is the first challenge.

The second challenge is that copying data files can take a while. However, after completing a checkpoint, the server may delete some log files that are no longer needed for crash recovery but are still required for the backup.

Without proper safeguards, the resulting backup will be unusable for recovery. Hot backups therefore require specialized tools. PostgreSQL provides a low-level interface that enables reliable backup implementation. This interface is used by both the built-in pg_basebackup tool and various third-party backup solutions.

https://postgrespro.com/docs/postgresql/16/continuous-archiving#BACKUP-BASE-BACKUP

Page 7: Replication Protocol

Purposes

  • backup and replication management
  • specifically, receiving a stream of WAL records
  • Handled by the wal_sender process
    • max_wal_senders = 10
    • wal_level = replica or logical, but not minimal
  • Connection
    • A role with the REPLICATION attribute or the SUPERUSER attribute
    • permission to connect granted in pg_hba.conf

Speaker notes

PostgreSQL provides a specialized protocol for managing both backup and replication (covered in a dedicated module). Among other things, it facilitates receiving a stream of WAL records generated by the server.

On the server side, replication protocol connections are handled by the wal_sender process. It behaves like a regular backend process that starts when a client connects, but instead of SQL, it processes special replication commands. The number of concurrent processes (wal_sender) is limited by the max_wal_senders parameter (default is 10).

The WAL level must be set to replica or higher. On minimal WAL level, operations like CREATE TABLE AS SELECT, CREATE INDEX, and COPY FROM bypass WAL logging entirely: durability is ensured by writing data directly to disk rather than keeping it in RAM.

This is sufficient for crash recovery and recovery from a cold backup, but not for hot backup.

To use the replication protocol, the client must connect to the server using a role that has the REPLICATION attribute, or as a superuser. In pg_hba.conf, you need to allow this role to connect to the replication database (which is actually a keyword, not a database name). Moreover, the permission for all is not enough, the connection to replication must be allowed explicitly.

https://postgrespro.com/docs/postgresql/16/protocol-replication

Page 8: Replication slot

A server object that guarantees the retrieval of WAL records

  • Tracks which record was last sent to the client
  • The number of slots is limited bymax_replication_slots = 10
  • Retains WAL segments until all records have been delivered to the client
    • Allows the client to retrieve data at its own pace, including the ability
    • to disconnect temporarily
    • WAL files are accumulated on the server when the client is disconnected
    • status monitoring: pg_replication_slots

Speaker notes

You can use replication slots to prevent the server from deleting WAL files prematurely while a checkpoint is being created. When streaming WAL records through a replication slot, the slot keeps track of what records were delivered to the client, and will not let the server remove the WAL file until the client has received all records from it.

Using a slot means the client doesn’t have to worry about the server deleting WAL files before they have been received properly. The client can even disconnect and then reconnect after some time to continue receiving WAL records from where it left off.

However, keep in mind that WAL files will accumulate on the server while the client is disconnected, so the server could run out of available disk space. Therefore, every created slot should be added to monitoring (the pg_replication_slots view) and slots that are no longer needed should be removed promptly.

The total number of slots that can be created is limited by the max_replication_slots configuration parameter (default is 10).

The use of slots is covered in more detail in the Replication module.

https://postgrespro.com/docs/postgresql/16/warm-standby#STREAMING-RE PLICATION-SLOTS

Page 9: Cluster Cloning

Source slide page 9: Cluster Cloning{#source-figure-009 caption=“Source slide page 9: Cluster Cloning”}

physical backups are created using pg_basebackup the new server is ready to start as soon as the backup completes

Speaker notes

To immediately deploy a new server from a backup, run pg_basebackup in plain format (–format=plain – by default) on the target server. It connects to the source server and copies files from it to the local directory specified by the –pgdata parameter.

Additional tablespaces are copied to the same absolute paths as on the source server. Paths can be reassigned by specifying matches in the – tablespace-mapping utility parameters. This is necessary if pg_basebackup runs on the source server.

With streaming delivery (–wal-method = stream – default), the utility establishes a second connection to stream WAL records generated by the running server to the local directory specified by the –waldir parameter.

By default, the second connection uses a temporary replication slot that is created on connection and deleted when pg_basebackup completes. However, you can also specify a permanent slot by name. The slot must exist at the time the utility is launched or be created by it.

In plain format, you can compress data on the server, reducing the network load. The transmitted data is then decompressed by the utility. But WAL records are not compressed in streaming mode.

https://postgrespro.com/docs/postgresql/16/app-pgbasebackup

Page 10: Cluster Cloning

Now let’s create a base backup of the running server alpha and use it to restore server beta.

student$ sudo pg_ctlcluster 16 alpha start
student$ psql backup_base

Permission for local connection via the replication protocol is already present in pg_hba.conf:

student$ psql -U postgres << EOF
SELECT type, database, user_name, address, auth_method
FROM pg_hba_file_rules() WHERE 'replication' = ANY(database);
EOF
 type  |   database    | user_name |  address  |  auth_method
-------+---------------+-----------+-----------+---------------
 local | {replication} | {all}     |           | trust
 host  | {replication} | {all}     | 127.0.0.1 | scram-sha-256
 host  | {replication} | {all}     | ::1       | scram-sha-256
(3 rows)

For the pg_basebackup tool to connect to the server as the student role, this role must have the REPLICATION attribute:

α=> \du student
                  List of roles
 Role name |             Attributes
-----------+-------------------------------------
 student   | Create role, Create DB, Replication

Stop beta and delete its data directory.

student$ sudo pg_ctlcluster 16 beta stop
student$ sudo rm -rf /var/lib/postgresql/16/beta

Create a backup with a quick checkpoint. Data will be compressed on the server side and then unpacked with the tool. This speeds up network transfer, but has no effect if the connection is purely local. The option –pgdata points to the directory to save the backup to. The directory is created automatically. If it already exists, it must be empty.

postgres$ pg_basebackup \
  --checkpoint=fast \
  --compress=server-zstd \
  --pgdata=/var/lib/postgresql/16/beta

The command runs with default values: –format=plain –wal-method=stream Write-ahead log files have been copied to the pg_wal subdirectory:

postgres$ ls -l /var/lib/postgresql/16/beta/pg_wal
total 16388
-rw------- 1 postgres postgres 16777216 Sep 16 23:55 000000010000000000000002
drwx------ 2 postgres postgres     4096 Sep 16 23:54 archive_status

Page 11: Integrity Verification

Data file verification

  • objective: to ensure that the backup receives consistent data
  • pg_basebackup and pg_checksums utilities
  • check relation file page checksums
  • Backup verification
    • objective: verify that the integrity of the data in the backup has not
    • deteriorated over time
    • manifest file backup_manifest
    • pg_verifybackup utility checks files from the manifest list

Speaker notes

Data can be corrupted both when writing to disk and during storage It is imperative to verify the integrity of data as it is written, but also that this integrity is not compromised over time.

Relation file checksum calculation is enabled by default when a cluster is initialized (or when running pg_checksums utility). The checksum is stored in the page header and updates whenever the the page changes. The old checksum and the new one are compared:

● each time the page is read into the buffer cache;

● by pg_basebackup when creating a backup;

● by pg_checksums in verification mode.

pg_basebackup creates a file backup_manifest and puts it into the backup file with information about files (names, sizes, calculated checksums) and log records (WAL start and end positions, time line), as well as the checksum of the manifest itself.

https://postgrespro.com/docs/postgresql/16/backup-manifest-format

The utility pg_verifybackup checks the backup against the manifest: the presence of files, the correctness of checksums, the ability to read and parse WAL records necessary for recovery.

https://postgrespro.com/docs/postgresql/16/app-pgverifybackup

You should still deploy the backup to make sure it works and check the health of the server, because the coincidence of checksums does not guarantee that the data was not damaged.

Page 12: Integrity Verification

The backup includes a manifest file – here is its beginning and end:

postgres$ head -5 /var/lib/postgresql/16/beta/backup_manifest
{ "PostgreSQL-Backup-Manifest-Version": 1,
"Files": [
{ "Path": "backup_label", "Size": 225, "Last-Modified": "2026-09-16 20:54:59 GMT",
"Checksum-Algorithm": "CRC32C", "Checksum": "9a280c44" },
{ "Path": "postgresql.auto.conf", "Size": 88, "Last-Modified": "2026-07-15 20:46:12 GMT",
"Checksum-Algorithm": "CRC32C", "Checksum": "536f950b" },
{ "Path": "pg_logical/replorigin_checkpoint", "Size": 8, "Last-Modified": "2026-09-16
20:54:59 GMT", "Checksum-Algorithm": "CRC32C", "Checksum": "c74b6748" },
postgres$ tail -6 /var/lib/postgresql/16/beta/backup_manifest
{ "Path": "global/pg_control", "Size": 8192, "Last-Modified": "2026-09-16 20:54:59 GMT",
"Checksum-Algorithm": "CRC32C", "Checksum": "43872087" }
],
"WAL-Ranges": [
{ "Timeline": 1, "Start-LSN": "0/2000028", "End-LSN": "0/2000100" }
],
"Manifest-Checksum": "41199043083bdc941bdc51a4610953d3c0b903911aef0bd0d043f5dfe5981320"}

Verify the backup integrity using the pg_verifybackup tool:

postgres$ /usr/lib/postgresql/16/bin/pg_verifybackup /var/lib/postgresql/16/beta
backup successfully verified

The tool uses the manifest to verify that all files are present and their checksums are correct, and that all WAL records required for recovery exist and are readable. Start the second server.

student$ sudo pg_ctlcluster 16 beta start

Now both servers run concurrently and independently. Check:

student$ psql -p 5433 -d backup_base
β=> INSERT INTO t VALUES ('...and this one on β.');
INSERT 0 1
β=> SELECT * FROM t;
               s
-------------------------------
 This row was inserted on α...
 ...and this one on β.
(2 rows)

Page 13: WAL Streaming Delivery

Source slide page 13: WAL Streaming Delivery{#source-figure-013 caption=“Source slide page 13: WAL Streaming Delivery”}

creating a backup for future use

Speaker notes

The tar format is used for archive backups. WAL streaming delivery is used by default ( wal-method=stream).

The cluster files will be saved as a base.tar file. Each additional tablespace is copied to a separate tar file with the same name as the tablespace OID. WAL segments are placed in a separate pg_wal.tar file.

The utility can perform compression both on the client side and on the server side (in the latter case, the pg_wal.tar file is not compressed). A suffix corresponding to the compression method will be added to the names of compressed tar archives. For example, when compressed using the gzip method, the main file will be called base.tar.gz.

To restore from such a backup, you’ll first need to extract the tar files to their proper locations. You can change the paths of tablespaces by editing the tablespace_map file before starting the server or using the –tablespace-mapping command line parameter.

Page 14: WAL File Delivery to Client

Source slide page 14: WAL File Delivery to Client{#source-figure-014 caption=“Source slide page 14: WAL File Delivery to Client”}

creating a backup for future use

Speaker notes

When creating a backup, you can use another mode – WAL file delivery (– wal-method = fetch).

In this case, no separate connection is created, and the WAL segments are copied and sent over the main connection. This happens after all cluster files have been backed up. During checkpoint creation, the server can delete WAL segments that are no longer needed for recovery. To prevent losing necessary files, set a sufficiently large value for the wal_keep_size parameter.

WAL segments are written to the base.tar file together with the rest of the cluster files.

  • The fetch mode is almost never used on the client. It remains as a legacy,
  • and the only viable use case for it now is sending WAL records compressed
  • which is impossible in streaming mode.

Page 15: Client-Side Backup

We are going to create a client-side backup. Compression will also be performed on the client.

student$ pg_basebackup \
  --checkpoint=fast \
  --compress=client-gzip:level=9 \
  --format=tar \
  --pgdata=/home/student/tmp/alpha-stream.backup

This command used streaming WAL delivery by default (–wal-method=stream). Let’s take a look at the resulting backup:

student$ ls -lh /home/student/tmp/alpha-stream.backup
total 5.2M
-rw------- 1 student student 220K Sep 16 23:55 backup_manifest
-rw------- 1 student student 5.0M Sep 16 23:55 base.tar.gz
-rw------- 1 student student  17K Sep 16 23:55 pg_wal.tar.gz

When streaming WAL records in tar format, a separate file is created for them:

student$ tar tf /home/student/tmp/alpha-stream.backup/pg_wal.tar.gz
000000010000000000000004

Now let’s create another client-side backup, but this time using the fetch delivery mode. In this mode, WAL segments are transferred over the same connection as the data. Let the server handle the compression.

student$ pg_basebackup \
  --checkpoint=fast \
  --wal-method=fetch \
  --compress=server-gzip:level=9 \
  --format=tar \
  --pgdata=/home/student/tmp/alpha-fetch.backup

In the resulting backup, WAL segments are included in the base.tar.gz file.

student$ ls -lh /home/student/tmp/alpha-fetch.backup
total 5.2M
-rw------- 1 student student 220K Sep 16 23:55 backup_manifest
-rw------- 1 student student 5.0M Sep 16 23:55 base.tar.gz
student$ tar tf /home/student/tmp/alpha-fetch.backup/base.tar.gz pg_wal
pg_wal/
pg_wal/000000010000000000000005
pg_wal/archive_status/000000010000000000000005.done

Page 16: WAL File Delivery to Server

Source slide page 16: WAL File Delivery to Server{#source-figure-016 caption=“Source slide page 16: WAL File Delivery to Server”}

creating a backup for future use the backup is created on the server, nothing is sent over the network

Speaker notes

You can create a backup on the server by running pg_basebackup on the client. No data is transmitted over the network.

To create a backup on the server from a client, use the command line parameter pg_basebackup –target = server: path, where path is the directory path in the server file system (default is –target=client).

This mode supports compression, but only on the server side, naturally.

When creating a backup on the server, you cannot use WAL streaming delivery because no WAL records are sent to the client. Therefore, the fetch delivery mode is used, and the accumulated WAL segments are placed in the base.tar file along with the rest of the cluster files.

Page 17: Server-Side Backup

Run pg_basebackup on the client while saving the backup on the server. Here, we must use the fetch delivery mode, so set the wal_keep_size parameter (and hope the value will be sufficient):

α=> ALTER SYSTEM SET wal_keep_size = '32MB';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha reload

Run pg_basebackup, specifying the directory on the server where the backup will be stored, and set compression mode to zstd.

student$ pg_basebackup \
  --checkpoint=fast \
  --wal-method=fetch \
  --target=server:/var/lib/postgresql/alpha.backup \
  --compress=server-zstd:level=12

We get a compressed backup in tar format (which is the default).

postgres$ ls -lh /var/lib/postgresql/alpha.backup
total 4.2M
-rw------- 1 postgres postgres 220K Sep 16 23:55 backup_manifest
-rw------- 1 postgres postgres 4.0M Sep 16 23:55 base.tar.zst

With the fetch delivery mode, the utility stores WAL segments in the main archive:

postgres$ tar tf /var/lib/postgresql/alpha.backup/base.tar.zst pg_wal
pg_wal/
pg_wal/000000010000000000000007
pg_wal/archive_status/000000010000000000000007.done

Delete the backup.

postgres$ rm -rf /var/lib/postgresql/alpha.backup

Page 18: Low-Level Perspective

Source slide page 18: Low-Level Perspective{#source-figure-018 caption=“Source slide page 18: Low-Level Perspective”}

full_page_writes is set to on during the backup process A checkpoint is performed at the very beginning (either fast or normal)

Speaker notes

The low-level backup algorithm is the same whether you’re using pg_basebackup or any third-party tools (which may be necessary since pg_basebackup only provides the most basic functionality).

  1. You need to notify the server that a backup is starting.

During the backup process, the system first enables the full_page_writes parameter: whenever a page is modified for the first time after a checkpoint, its complete image is written to the WAL. During recovery, WAL records will be applied not to pages in the file (due to non-atomic I/O, parts of the page may be mismatched), but to the page images from WAL.

Second, a checkpoint is performed. There are two checkpoint modes: fast (which can cause the I/O load to peak) and spread (controlled by the checkpoint_completion_target parameter).

  1. Once the checkpoint completes, you can copy the data files using any method that works for you.

  2. Once you’ve copied everything, you need to let the server know that the backup is complete.

  3. Additionally, you need to ensure that all WAL records generated from the start of the backup until its completion are included in the backup.

Page 19: Backup API

Backup start: pg\_backup\_start

  • performs a checkpoint and sets full_page_writes
  • returns the WAL starting position
  • Backup end: pg_backup_stop
    • returns the WAL ending position
  • Copying files
    • either the replication protocol or any OS tools
  • Copying the WAL from the starting to ending position
    • replication protocol + slot or OS tools + wal_keep_size

Speaker notes

You can use this backup algorithm to build your own custom backup utility.

The backup starts by calling the pg_backup_start function. It sets the full_page_writes parameter and performs a checkpoint, as discussed earlier. The function returns a specific position in the WAL. Records starting from that position must be included into the backup.

At the end of the backup, the pg_backup_stop function is called. It returns the final position in the WAL.

All WAL records between the starting and ending positions need to be included into the backup. Since the server may delete WAL files that aren’t needed for recovery during operation, you should either use the replication protocol with a replication slot to receive records during the backup process (like pg_basebackup does in –wal-method=stream mode), or alternatively — though less convenient — copy the files using OS tools after setting the wal_keep_size parameter.

You can copy files in parallel, use rsync, and so on.

https://postgrespro.com/docs/postgresql/16/continuous-archiving#BACKUP-LOWLEVEL-BASE-BACKUP

Page 20: Takeaways

A base backup is a complete copy of the database cluster, including tablespaces, along with the WAL needed to restore consistency

  • Hot backup allows the server to remain running during the backup process
  • Tools
    • replication protocol
    • replication slot
    • low-level backup API

Page 21: Practice

1. In the first cluster, create a tablespace and a database containing a table within that tablespace.

    1. Create a compressed tar-format backup of the cluster using pg_basebackup. 3. Deploy a second cluster from this backup. Place the tablespace in a different directory by modifying the tablespace_map file. 4. Start the second server and verify that it’s working properly.

Speaker notes

After the practice is complete, delete the database and tablespace in both clusters.

Page 22: 1. Tablespace and Database

student$ sudo mkdir /var/lib/postgresql/ts_dir
student$ sudo chown postgres /var/lib/postgresql/ts_dir

Only a superuser can create tablespaces:

α=> \c - postgres
You are now connected to database "student" as user "postgres".
α=> CREATE TABLESPACE ts OWNER student LOCATION '/var/lib/postgresql/ts_dir';
CREATE TABLESPACE
α=> \c - student
You are now connected to database "student" as user "student".
α=> CREATE DATABASE backup_base;
CREATE DATABASE
α=> \c backup_base
You are now connected to database "backup_base" as user "student".
α=> CREATE TABLE t(s text) TABLESPACE ts;
CREATE TABLE
α=> INSERT INTO t VALUES ('Hello, world!');
INSERT 0 1

2. Base Backup

Run pg_basebackup as the student with tar compression:

student$ pg_basebackup \
  --checkpoint=fast \
  --format=tar \
  --compress=client-gzip \
  --pgdata /home/student/tmp/backup
student$ ls -l /home/student/tmp/backup
total 5352
-rw------- 1 student student     330 Sep 17 00:07 16409.tar.gz
-rw------- 1 student student  224520 Sep 17 00:07 backup_manifest
-rw------- 1 student student 5229317 Sep 17 00:07 base.tar.gz
-rw------- 1 student student   17101 Sep 17 00:07 pg_wal.tar.gz

3. Recovery from a Base Backup

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.

Clear the main data directory on the beta cluster. The directory must be accessible only by its owner.

student$ sudo rm -rf /var/lib/postgresql/16/beta
student$ sudo mkdir /var/lib/postgresql/16/beta
student$ sudo chown postgres /var/lib/postgresql/16/beta
student$ sudo chmod 700 /var/lib/postgresql/16/beta

The directory for the ts tablespace on the server that will be restored from the backup:

student$ sudo mkdir /var/lib/postgresql/ts_beta_dir
student$ sudo chown postgres /var/lib/postgresql/ts_beta_dir

Deploy the backup:

student$ sudo tar xf /home/student/tmp/backup/base.tar.gz -C /var/lib/postgresql/16/beta

Page 23: Page 23

student$ sudo tar xf /home/student/tmp/backup/16409.tar.gz -C /var/lib/postgresql/ts_beta_dir
student$ sudo tar xf /home/student/tmp/backup/pg_wal.tar.gz -C /var/lib/postgresql/16/beta/pg_wal

The pg_tblspc directory is currently empty:

student$ sudo ls /var/lib/postgresql/16/beta/pg_tblspc/

The symbolic link will be created when the server starts, based on the tablespace_map file (which was located inside base.tar):

student$ sudo cat /var/lib/postgresql/16/beta/tablespace_map
16409 /var/lib/postgresql/ts_dir

Update the tablespace path in this file:

student$ sudo sed -i 's/ts_dir/ts_beta_dir/' /var/lib/postgresql/16/beta/tablespace_map

4. Startup and Verification

Start the server.

student$ sudo pg_ctlcluster 16 beta start
student$ sudo ls -l /var/lib/postgresql/16/beta/pg_tblspc/
total 0
lrwxrwxrwx 1 postgres root 31 Sep 17 00:07 16409 -> /var/lib/postgresql/ts_beta_dir
student$ psql -p 5433 -d backup_base
β=> \c backup_base
You are now connected to database "backup_base" as user "student".
β=> SELECT * FROM t;
       s
---------------
 Hello, world!
(1 row)

Cleanup

Delete the backup.

student$ rm -rf /home/student/tmp/backup

To delete the database, you must first disconnect from it.

α=> \c student
You are now connected to database "student" as user "student".
α=> DROP DATABASE backup_base;
DROP DATABASE
α=> DROP TABLESPACE ts;
DROP TABLESPACE
student$ sudo rm -rf /var/lib/postgresql/ts_dir

And on the second server:

β=> \c student
You are now connected to database "student" as user "student".
β=> DROP DATABASE backup_base;
DROP DATABASE
β=> DROP TABLESPACE ts;
DROP TABLESPACE
student$ sudo rm -rf /var/lib/postgresql/ts_beta_dir

Page 24: Practice+

  1. Load the basebackup_to_shell module to create a base backup on the server using a user-defined command. This command should compress the tar backup using the bzip2 utility. 2. Back up the server in tar format. Make sure that the archive is compressed by bzip2. 3. Restore the beta cluster from the resulting compressed archive and verify that it works.

Speaker notes

  1. To load the module, add the library of the same name to the shared_preload_libraries parameter. With the basebackup_to_shell.command parameter, specify the command to run when backing up:
bzip2 -c> archive.tar.bz2

https://postgrespro.com/docs/postgresql/16/basebackup-to-shell

  1. To load the module for backup, specify pg_basebackup –target=shell

  2. To extract files from the tar archive compressed by the bzip2 utility, use the command:

tar xjf archive.tar.bz2 -C destination directory

After completing the practice, delete the created databases and the backup.

Page 25: 1. Loading the Module

Create a configuration for the basebackup_to_shell module in the configuration file.

postgres$ cat << EOF > /etc/postgresql/16/alpha/conf.d/basebackup_to_shell.conf
shared_preload_libraries = basebackup_to_shell
basebackup_to_shell.command = 'bzip2 -c > /var/lib/postgresql/archive.backup/%f.bz2'
EOF

Restart the server.

student$ sudo pg_ctlcluster 16 alpha restart

Check the configuration.

student$ psql
α=> SHOW shared_preload_libraries;
 shared_preload_libraries
--------------------------
 basebackup_to_shell
(1 row)
α=> SHOW basebackup_to_shell.command;
             basebackup_to_shell.command
------------------------------------------------------
 bzip2 -c > /var/lib/postgresql/archive.backup/%f.bz2
(1 row)

Create a database and a table in it.

α=> CREATE DATABASE backup_base;
CREATE DATABASE
α=> \c backup_base
You are now connected to database "backup_base" as user "student".
α=> CREATE TABLE t AS SELECT now() AS creatime;
SELECT 1

2. Backup

Create a directory for the backup.

postgres$ mkdir /var/lib/postgresql/archive.backup

Perform the backup. Streaming mode is not available, so use the WAL fetch delivery mode.

student$ pg_basebackup \
  --checkpoint=fast \
  --wal-method=fetch \
  --target=shell
postgres$ ls -l /var/lib/postgresql/archive.backup
total 4440
-rw------- 1 postgres postgres    9927 Sep 17 00:07 backup_manifest.bz2
-rw------- 1 postgres postgres 4531299 Sep 17 00:07 base.tar.bz2

3. Recovery from Backup

Delete the beta cluster files.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ mkdir /var/lib/postgresql/16/beta

Page 26: Page 26

Source slide page 26: Page 26{#source-figure-026 caption=“Source slide page 26: Page 26”}

postgres$ chmod 700 /var/lib/postgresql/16/beta

Extract the files from the archive.

postgres$ tar xjf /var/lib/postgresql/archive.backup/base.tar.bz2 -C /var/lib/postgresql/16/beta

Check whether the data has been restored.

student$ sudo pg_ctlcluster 16 beta start
student$ psql -p 5433 -d backup_base
β=> SELECT * FROM t;
           creatime
-------------------------------
 2026-09-17 00:07:40.876136+03
(1 row)

Cleanup

α=> \c student
You are now connected to database "student" as user "student".
α=> DROP DATABASE backup_base;
DROP DATABASE
β=> \c student
You are now connected to database "student" as user "student".
β=> DROP DATABASE backup_base;
DROP DATABASE
student$ sudo pg_ctlcluster 16 beta stop
postgres$ rm -rf /var/lib/postgresql/archive.backup

03 Lesson 03: Backup / WAL Archive

Native Markdown source page: Backup / WAL Archive
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: No matching course-page link was captured in the source inventory. Archive: DBA3-handouts-16-en-20260917.zip PDF pages: 31

Page 1: Backup / WAL Archive

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Alexey Beresnev Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • File-Based Archiving – Continuous Archiving
  • Stream Archiving – pg_receivewal
  • Recovery from Archive
  • Archive Cleanup

Page 3: Continuous Archiving

Source slide page 3: Continuous Archiving{#source-figure-003 caption=“Source slide page 3: Continuous Archiving”}

  • archive_mode = on
  • archive_command
  • archive_library
  • archive_timeout

Speaker notes

The base backup discussed in the previous topic contains all the WAL records needed to restore data consistency. The resulting backup is called standalone because it contains all the data necessary for recovery up to the point in time when the backup was created.

If you add new WAL files generated by the server to the standalone backup, you can restore the cluster to an arbitrary point in time. In practice, the WAL segments are not copied to the backup file itself, but to a separate archive. This is done by the archiver process (if archive_mode = on).

There are two ways to archive a WAL segment:

  1. Use an OS command. When switching to the next WAL segment, the process executes the command stored in the archive_command parameter.

  2. Use the archiving module functions (PostgreSQL 15 and up). The module name is set in the archive_library parameter. https://postgrespro.com/docs/postgresql/16/archive-modules

If the server load is light, the archive_timeout parameter is commonly used to set the maximum time that can pass before switching to a new WAL segment. Switching can also be done manually using the pg_switch_wal function.

https://postgrespro.com/docs/postgresql/16/continuous-archiving

Page 4: archive_command

An operating system command that archives completed WAL segments

  • copies file%p to archive named%f,
  • the archive structure can be organized in any way you choose.
  • must return 0 only if successful
  • must not overwrite files that already exist in the archive
  • must ensure data is written to persistent storage
  • conveniently supports custom scripts
  • Monitoring
    • pg_stat_archiver view shows current status
    • can enable the message collector (logging_collector = on) and write messages to
    • the server log

Speaker notes

The archive command must copy the specified file to an archive. The archive can be organized in any way you choose. For example, this could be a file system on a separate server.

The command must return a zero exit status only when it succeeds. In this case, the copied segment can be deleted by the checkpoint process. With a non-zero status, the segment that failed to copy and subsequent segments are not deleted, and the server periodically retries the archive command until it receives 0.

The command should never overwrite existing files, as this likely indicates an error, and overwriting a file could invalidate the archive.

To ensure reliability, the command must guarantee that the file is written to persistent storage; otherwise, if the archiving command fails, you could lose a file segment if the PostgreSQL server deletes it in the meantime.

The current archiving status is shown in the pg_stat_archiver view. The message collector process logging_collector can conveniently write error and other messages from archive_command execution to the server log.

If complex logic is required, it can be scripted in a separate file, and the archiving command can be set to run it.

https://postgrespro.com/docs/postgresql/16/runtime-config-wal#GUC-ARCHI VE-COMMAND

Page 5: Setting up continuous archiving

We will store the archive in the /var/lib/postgresql/archive directory. It must be accessible to the PostgreSQL owner user.

postgres$ mkdir /var/lib/postgresql/archive

In practice, you can keep the archive on a separate server or disk system accessible over the network. Let’s enable archiving mode and set the command for copying completed WAL segments.

α=> ALTER SYSTEM SET archive_mode = on;
ALTER SYSTEM

In archive_command, we first check whether a file with the specified name already exists in the archive and only copy it if it’s not there.

α=> ALTER SYSTEM SET archive_command =
'test ! -f /var/lib/postgresql/archive/%f && cp %p /var/lib/postgresql/archive/%f';
ALTER SYSTEM

You can specify any command in archive_command, as long as it returns 0 only when successful. For example, you can set up compression of archived segments:

SET archive_command =
'test ! -f /var/lib/postgresql/archive/%f && gzip <%p >/var/lib/postgresql/archive/%f';

Ideally, the archiving command should perform a sync to ensure the file is safely written to persistent storage. Otherwise, in case of a failure, the file might be lost from the archive while the server has already deleted it from the pg_wal directory. It’s usually convenient to put all the necessary archiving logic in a separate script and call the entire script:

SET archive_command = 'archive.sh "%f" "%p"';

Changing archive_mode requires a server restart.

student$ sudo pg_ctlcluster 16 alpha restart
student$ psql

The archiver process now exists:

α=> SELECT pid, backend_type, backend_start
FROM pg_stat_activity
WHERE backend_type='archiver';
  pid  | backend_type |         backend_start
-------+--------------+-------------------------------
 71779 | archiver     | 2026-09-16 23:56:16.851718+03
(1 row)

Let’s verify that archiving is working. Create a database and a table.

α=> CREATE DATABASE backup_archive;
CREATE DATABASE
α=> \c backup_archive
You are now connected to database "backup_archive" as user "student".
α=> CREATE TABLE t(s text);
CREATE TABLE
α=> INSERT INTO t VALUES ('Row #1');
INSERT 0 1

Here’s the WAL segment currently in use:

α=> SELECT pg_walfile_name(pg_current_wal_lsn());
     pg_walfile_name
--------------------------
 000000010000000000000001
(1 row)

Page 6: Page 6

To fill out the file, we would need to perform a large number of operations. For testing purposes, it’s easier to force a segment switch:

α=> SELECT pg_switch_wal();
 pg_switch_wal
---------------
 0/1FB6108
(1 row)
α=> INSERT INTO t VALUES ('Another row');
INSERT 0 1

The segment has switched:

α=> SELECT pg_walfile_name(pg_current_wal_lsn());
     pg_walfile_name
--------------------------
 000000010000000000000002
(1 row)

The previous one should have made it to the archive. Check:

student$ sudo ls -lh /var/lib/postgresql/archive
total 16M
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000001

The current archiving status is shown in the pg_stat_archiver view:

α=> SELECT * FROM pg_stat_archiver \gx
-[ RECORD 1 ]------+------------------------------
archived_count     | 1
last_archived_wal  | 000000010000000000000001
last_archived_time | 2026-09-16 23:56:24.776632+03
failed_count       | 0
last_failed_wal    |
last_failed_time   |
stats_reset        | 2026-07-15 23:46:12.428787+03

File-based archiving is now configured and working.

Page 7: pg_receivewal

Source slide page 7: pg_receivewal{#source-figure-007 caption=“Source slide page 7: pg_receivewal”}

  • max_wal_senders
  • max_replication_slots

Speaker notes

Alternatively, you can populate the archive using the replication protocol and the pg_receivewal utility.

Typically, the utility runs on a separate server that stores the archive. It generates files in the same way as the main server itself, and writes them to the specified directory. Incomplete segments have the suffix .partial. Cache synchronization with the file system occurs only when the WAL segment is closed, but you can make it synchronous with the – synchronous parameter.

The utility can compress WAL segments. Use the parameter –compress with the algorithm (gzip or lz4) and the compression level as arguments.

When running the utility for the first time (when the archive is empty), archiving starts from the beginning of the current WAL segment. Otherwise, it starts from the beginning of the segment following the last completed segment in the archive. When using a replication slot, this ensures that there are no gaps in the archive, even if the utility was disabled for a while.

By default, the utility receives records indefinitely, but you can set it to terminate when receiving a record with a specific LSN: –endpos = lsn.

Note that the utility does not start automatically as a service and cannot be a daemon.

https://postgrespro.com/docs/postgresql/16/app-pgreceivewal

Page 8: Streaming Archive

To set up archive population via the streaming replication protocol, let’s first stop file-based archiving.

α=> ALTER SYSTEM RESET archive_mode;
ALTER SYSTEM
α=> ALTER SYSTEM RESET archive_command;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

The current state of the archive.

student$ sudo ls -lh /var/lib/postgresql/archive
total 32M
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000001
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000002

First, let’s have the pg_receivewal utility create a slot to ensure all WAL records are received.

postgres$ /usr/lib/postgresql/16/bin/pg_receivewal --create-slot --slot=archive

Start it in the background in archiving mode. Upon detecting existing files in the archive, the utility will request the next segment from the server to ensure there are no gaps in the archive.

postgres$ /usr/lib/postgresql/16/bin/pg_receivewal -D /var/lib/postgresql/archive --slot=archive &

Let’s add a ton of rows to the table and then delete them.

student$ psql -d backup_archive
α=> INSERT INTO t SELECT 'Hello again.' FROM generate_series(1,200_000);
INSERT 0 200000
α=> DELETE FROM t WHERE s = 'Hello again.';
DELETE 200000
α=> VACUUM t;
VACUUM

New files have appeared in the archive.

student$ sudo ls -lh /var/lib/postgresql/archive
total 64M
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000001
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000002
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000003
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000004.partial

The last file most likely has a .partial suffix – it is currently being written to. Stream archiving is now configured and working.

Page 9: Base backup

Source slide page 9: Base backup{#source-figure-009 caption=“Source slide page 9: Base backup”}

Speaker notes

If we have a configured write-ahead log archive, then WAL files become optional in the base backup — since they can be retrieved from the archive during recovery.

Therefore, pg_basebackup can be run with –wal-method=none.

(But there is no harm in having WAL files in the base copy, except for the space they occupy. In fact, they enable system recovery if the archive becomes unavailable.)

Page 10: Base Backup

Since the archive is populated automatically, let’s instruct pg_basebackup not to include WAL files in the backup:

student$ pg_basebackup \
  --checkpoint=fast \
  --wal-method=none \
  --pgdata /home/student/tmp/backup
NOTICE:  WAL archiving is not enabled; you must ensure that all required WAL segments are
copied through other means to complete the backup

There are no WAL segments in the backup’s pg_wal directory:

student$ ls -l /home/student/tmp/backup/pg_wal/
total 4
drwx------ 2 student student 4096 Sep 16 23:56 archive_status

The archive, however, has gained new files:

student$ sudo ls -lh /var/lib/postgresql/archive
total 96M
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000001
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000002
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000003
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000004
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000005
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000006.partial

Page 11: Recovery

Source slide page 11: Recovery{#source-figure-011 caption=“Source slide page 11: Recovery”}

recovery.signal

  • signals recovery from backup tablespace_map
    • tablespace paths
  • backup_label
    • starting position and other details
  • postgresql.conf
    • restore_command
    • target recovery point
    • etc.

Speaker notes

The recovery process is controlled by three files and parameters.

The recovery.signal file is created manually. If it exists, the server starts in recovery mode. The file’s contents are ignored. If recovery.signal is missing, PostgreSQL assumes it’s performing automatic crash recovery. If the file exists, the server understands that a user-controlled restore from a backup is taking place, and checks two other files that are generated when a base backup is created:

● The tablespace_map file contains paths for table spaces. If the symlincs in pg_tblspc are missing (which is true for tar format), the server will create them at startup based on the information from tablespace_map.

We’ve already encountered this file in the “Base Backup” topic.

● The backup_label file stores the backup name, creation time, and, most importantly, the name of the WAL segment and the position in it to start recovery from.

During recovery, the startup process reads files from the archive using the command from restore_command and applies WAL records from the starting position specified in the label file up to the target recovery position.

Page 12: Recovery Configuration

Let’s start the recovery configuration by creating a signal file.

student$ touch /home/student/tmp/backup/recovery.signal

When the base backup was created, a backup_label file was generated.

student$ cat /home/student/tmp/backup/backup_label
START WAL LOCATION: 0/5000028 (file 000000010000000000000005)
CHECKPOINT LOCATION: 0/5000060
BACKUP METHOD: streamed
BACKUP FROM: primary
START TIME: 2026-09-16 23:56:35 MSK
LABEL: pg_basebackup base backup
START TIMELINE: 1

The key piece of information in this file is the starting point for recovery (START WAL LOCATION). Theoretically, recovery can begin from an earlier position (but not a later one), though this will take more time. Since our cluster has no additional tablespaces, the tablespace_map file is not necessary. All that remains is to set a restore_command and other parameters in the configuration file.

Page 13: restore_command

OS command restoring WAL segment

  • performs the reverse of archive_command
copies file%f from archive to server at path%p
  • must return 0 only if successful
  • (can be called for non-existent files)
  • Monitoring
    • log_startup_progress_interval = 10s
    • logging of startup process status

Speaker notes

The restore_command parameter specifies the command to “reverse” the archive_command. This command must copy the file from the archive to the pg_wal directory and return a status of 0 only when successful. This is very important, since during the recovery process the server can execute a restore command for files that will not be in the archive – this is not an error, but the command must end with non-zero status.

Starting with version PostgreSQL 14, changing the restore_command parameter does not require a server restart.

https://postgrespro.com/docs/postgresql/16/runtime-config-wal#RUNTIME-C ONFIG-WAL-ARCHIVE-RECOVERY

In PostgreSQL 15 and higher, the status of the recovery process can be monitored in the server message log with the log_startup_progress_interval option enabled. It logs the progress every 10 seconds by default.

In earlier versions, information about the current recovery state was displayed only in the name of the startup unix process.

Page 14: Restartpoint

Checkpoint equivalent for recovery

  • flush dirty buffers
  • write to control file
  • remove unnecessary WAL segments
  • Performed when a control point record is applied
    • if at least checkpoint_timeout has elapsed since the restartpoint
    • or WAL volume approaching max_wal_size
  • Monitoring
    • log_checkpoints
    • pg_stat_bgwriter

Speaker notes

When the startup process applies WAL records during recovery, the data in the server’s shared memory changes in the same way as in normal operation: dirty buffers appear. WAL recods must be retained in case a failure occurs and recovery must restart. To limit the amount of stored records and not start recovery from the very beginning, the server periodically performs restartpoints, resetting, as at a checkpoint, dirty buffers of shared memory structures: buffer cache, clog cache and others. If the restartpoint completes successfully, the server writes to the control file and deletes the WAL segments that entirely precede the restartpoint start.

The restartpoint is executed when the startup process encounters a checkpoint record in the WAL, if at least checkpoint_timeout time has passed since the last restartpoint, or if the WAL size approaches the max_wal_size value. In other words, the restartpoint is triggered by the checkpoint WAL record, but if none of the conditions are met, the record is skipped.

If log_checkpoints is enabled, all restartpoints will be recorded in the message log, and the pg_stat_bgwriter view will show accumulated statistics.

https://postgrespro.com/docs/postgresql/16/wal-configuration

Page 15: Recovery Target

Source slide page 15: Recovery Target{#source-figure-015 caption=“Source slide page 15: Recovery Target”}

Point-in-Time Recovery (PITR)

  • recovery_target = ‘immediate’
  • restore consistency only
  • recovery_target_name = ’name'
  • to a named restore point created with
  • pg_create_restore_point(’name')
  • recovery_target_time = ’timestamp’ up to the specified timestamp
  • recovery_target_xid = ‘xid’
  • to the specified transaction
  • recovery_target_lsn = ’lsn'
  • to the specified LSN
  • recovery_target_inclusive = on|off
  • whether to include the target point
  • recovery_target_action = pause

pause

  • promote
  • accepts connections
  • shutdown
  • pauses

Speaker notes

If the recovery target is not set, the cluster will be restored as close as possible to the time of failure. However, you can stop the recovery process at any other point by setting a recovery target.

● The recovery_target = ‘immediate’ parameter will stop recovery as soon as consistency is achieved. This is effectively the same as recovering from a base backup without using the archive.

● The recovery_target_name parameter lets you specify a named recovery point that was previously created using the pg_create_restore_point function.

● The recovery_target_time parameter allows you to specify any point in time (timestamp), recovery_target_xid allows you to specify any transaction, and recovery_target_lsn allows you to specify an LSN. The recovery_target_inclusive parameter lets you choose whether to include or exclude the target point itself.

The recovery_target_action option allows you to select a state in which the server will be on reaching the target point:

● pause suspends recovery. The administrator can query the data to verify whether the recovery target was chosen correctly, and then either complete the recovery by calling the pg_wal_replay_resume function, or set a new target, restart the server, and continue applying WAL records.

● With promote, the server will immediately be able to accept connection requests.

● shutdown will stop the server. In this case, the recovery.signal file is not removed.

Page 16: Completion of Recovery

Source slide page 16: Completion of Recovery{#source-figure-016 caption=“Source slide page 16: Completion of Recovery”}

remove recovery.signal

  • backup_label backup_label.old
  • perform recovery_end_command
  • problems are diagnosed using the server log

Speaker notes

Once recovery is complete, the startup process terminates, postmaster launches the remaining service processes needed for the server instance to operate, and the server begins running in normal mode. The recovery.signal file is deleted, and backup_label is renamed to backup_label.old.

In the recovery_end_command parameter, you can specify a command that will be executed at the end of recovery. These may be file system operations, archive cleanup, notifications, etc.

The server may fail to start. In this case, no processes will be running in the operating system (and the postmaster.pid file will be absent), while the recovery.signal file will remain in place. The reasons can be found in the server log (for example, invalid configuration parameters). Once the issue has been resolved, restart the server.

Important note: if continuous archiving was set up on the origin server, it must either be disabled on the restored server or redirected to a different archive location.

Page 17: Recovery from a Base Backup

Let’s configure recovery settings on beta. In the simplest case, all you need is a restore command that copies the specified WAL segment from the archive to the specified path:

student$ echo "restore_command = 'cp /var/lib/postgresql/archive/%f %p'" \
  >> /home/student/tmp/backup/postgresql.auto.conf

If no recovery target is specified (via one of the recovery_target* parameters), WAL records from all files in the archive will be applied to the base backup. Place the backup in the beta server’s data directory and start it.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
student$ sudo rm -rf /var/lib/postgresql/16/beta
student$ sudo mv /home/student/tmp/backup /var/lib/postgresql/16/beta
student$ sudo chown -R postgres:postgres /var/lib/postgresql/16/beta
student$ sudo pg_ctlcluster 16 beta start

After a successful recovery, the recovery.signal file is removed and the label file is renamed to backup_label.old:

student$ sudo ls -l /var/lib/postgresql/16/beta | egrep 'recovery|backup_label'
-rw------- 1 postgres postgres    225 Sep 16 23:56 backup_label.old

Since all WAL records have been applied, the beta server immediately starts accepting connections. Let’s verify what was restored in the table:

student$ psql -p 5433 -d backup_archive
β=> SELECT * FROM t;
      s
-------------
 Row #1
 Another row
(2 rows)

The server’s cumulative statistics contain information about restart points:

β=> SELECT checkpoints_timed, checkpoints_req, checkpoint_write_time, checkpoint_sync_time, buffers_checkpoint
FROM pg_stat_bgwriter \gx
-[ RECORD 1 ]---------+---
checkpoints_timed     | 0
checkpoints_req       | 1
checkpoint_write_time | 29
checkpoint_sync_time  | 15
buffers_checkpoint    | 0

Page 18: Timelines

Source slide page 18: Timelines{#source-figure-018 caption=“Source slide page 18: Timelines”}

Sequential number, +1 with each recovery

  • Timeline number N is included in WAL segment name
  • Historical data is stored in the pg_wal/N.history and archived
  • recovery_target_timeline = ’latest’ restore to the last timeline (by default)
  • recovery_target_timeline = ‘current’ restore to the current timeline
  • recovery_target_timeline = ‘1’ restore to the specified timeline

Speaker notes

After restoring to a point in the past, the server begins to generate new WAL records. The record IDs will repeat the record IDs “from the past.” To preserve these segments and maintain the ability to recover to different points in time, PostgreSQL introduces the concept of timelines. After each recovery using recovery.signal, the timeline number increments, and this number becomes part of the WAL segment numbering.

Timelines form a tree structure, as shown in the diagrams. Timeline 1 is the initial timeline. A recovery was performed to a point in the past, creating timeline 2.

When performing recovery with the recovery_target_timeline = ’latest’ parameter (the default), WAL replay will follow the second (newer) timeline once it reaches the branch point.

Settingrecovery_target_timeline = ‘1’ will continue recovery along timeline 1 (the specified timeline), while recovery_target_timeline = ‘current’ will follow the current timeline (which in this case is also timeline 1).

PostgreSQL retrieves branch point information from history files located at pg_wal/N.history. Therefore, these files should never be removed from the archive.

Page 19: Timelines

What happened to the timeline after recovery?

β=> SELECT pg_walfile_name(pg_current_wal_lsn());
     pg_walfile_name
--------------------------
 000000020000000000000006
(1 row)

The number incremented by one. A history file corresponding to this branch has appeared in the pg_wal directory:

postgres$ ls /var/lib/postgresql/16/beta/pg_wal/00000002.history
/var/lib/postgresql/16/beta/pg_wal/00000002.history

It contains information about the branch points through which we arrived at this timeline:

student$ sudo cat /var/lib/postgresql/16/beta/pg_wal/00000002.history
1
0/6000000
no recovery target specified

PostgreSQL uses these files when a timeline is specified in the recovery_target_timeline parameter. History files must be archived alongside WAL file segments and should not be removed from the archive.

Page 20: Archive Cleanup

Source slide page 20: Archive Cleanup{#source-figure-020 caption=“Source slide page 20: Archive Cleanup”}

Speaker notes

The archive grows over time and needs to be cleaned of unnecessary files every now and then.

The pg_archivecleanup tool does just that. It takes a WAL segment name and deletes all preceding segments.

https://postgrespro.com/docs/postgresql/16/pgarchivecleanup

The question is which archive position to use as the cutoff point.

If you are only using the archive to restore from a single base backup, you can clean up as soon as you update the backup.

However, if the archive needs to support recovery from multiple base backups, you’ll need to either write a custom cleanup script or, better yet, use a third-party backup tool like pg_probackup that offers flexible backup retention policies.

Page 21: Archive Cleanup

The first server’s archive currently contains unnecessary files that were added before the base backup was created:

student$ sudo ls -lh /var/lib/postgresql/archive
total 96M
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000001
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000002
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000003
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000004
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000005
-rw------- 1 postgres postgres 16M Sep 16 23:57 000000010000000000000006.partial

The pg_archivecleanup utility takes the archive path and the name of the last WAL segment to keep (which can be found in the backup_label file).

student$ sudo head -n 1 /var/lib/postgresql/16/beta/backup_label.old
START WAL LOCATION: 0/5000028 (file 000000010000000000000005)
student$ sudo pg_archivecleanup /var/lib/postgresql/archive 000000010000000000000005

Let’s check which files remain in the archive:

student$ sudo ls -lh /var/lib/postgresql/archive
total 32M
-rw------- 1 postgres postgres 16M Sep 16 23:56 000000010000000000000005
-rw------- 1 postgres postgres 16M Sep 16 23:57 000000010000000000000006.partial

The archive has been cleaned up. The utility can be called immediately after creating the base backup by placing both commands in a script.

Page 22: Takeaways

Physical backups: base backup + WAL archive

  • Good for
    • ongoing periodic backups
    • crash recovery with minimal data loss
    • point-in-time recovery
    • backing up large volumes of data
  • Not so good for
    • long-term storage
  • Not suitable for
    • migration to a different platform

Page 23: Practice

1. On the alpha server, create a database and a data table. 2. Set up continuous archiving. 3. Make a copy of the cluster without log files. 4. Insert a few more rows into the table and verify that the current WAL segment has been archived.

    1. Restore the beta server from the backup, specifying only the restore command in the configuration.
    1. Restore the beta server again from the same backup, this time with immediate as the recovery target.

Speaker notes

After completing the practice tasks, delete the databases and reset the configuration.

Page 24: 1. Database and Table

α=> CREATE DATABASE backup_archive;
CREATE DATABASE
α=> \c backup_archive
You are now connected to database "backup_archive" as user "student".
α=> CREATE TABLE t(s text);
CREATE TABLE
α=> INSERT INTO t VALUES ('Hello, world!');
INSERT 0 1

2. Setting Up Continuous Archiving

postgres$ mkdir /var/lib/postgresql/archive
α=> ALTER SYSTEM SET archive_mode = on;
ALTER SYSTEM
α=> ALTER SYSTEM SET archive_command = 'test ! -f /var/lib/postgresql/archive/%f && cp %p /var/lib/postgresql/archive/%f';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

3. Base Backup

Since we’ll need to restore from the same backup twice, let’s create it in compressed tar format.

student$ pg_basebackup \
  --checkpoint=fast \
  --wal-method=none \
  --format=tar \
  --gzip \
  --pgdata=/home/student/tmp/backup
NOTICE:  all required WAL segments have been archived
student$ ls -l /home/student/tmp/backup
total 5328
-rw------- 1 student student  224439 Sep 17 00:08 backup_manifest
-rw------- 1 student student 5229378 Sep 17 00:08 base.tar.gz

4. Populating a Table With Rows

student$ psql -d backup_archive
α=> INSERT INTO t VALUES ('Another row');
INSERT 0 1

Switch the segment:

α=> SELECT pg_walfile_name(pg_current_wal_lsn()), pg_switch_wal();
     pg_walfile_name      | pg_switch_wal
--------------------------+---------------
 000000010000000000000003 | 0/3006640
(1 row)

Check the archiving status:

α=> SELECT last_archived_wal FROM pg_stat_archiver;
    last_archived_wal
--------------------------
 000000010000000000000003
(1 row)

5. Recovery from a Base Backup

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.

Page 25: Page 25

postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ mkdir /var/lib/postgresql/16/beta
postgres$ chmod 700 /var/lib/postgresql/16/beta
student$ sudo tar xzf /home/student/tmp/backup/base.tar.gz -C /var/lib/postgresql/16/beta

To make it simple, disable continuous archiving on the standby server and set just the restore command:

postgres$ echo "restore_command = 'cp /var/lib/postgresql/archive/%f %p' " \
  | tee /var/lib/postgresql/16/beta/postgresql.auto.conf
restore_command = 'cp /var/lib/postgresql/archive/%f %p'

Create recovery.signal.

postgres$ touch /var/lib/postgresql/16/beta/recovery.signal

Start the server.

student$ sudo pg_ctlcluster 16 beta start
student$ psql -p 5433 -d backup_archive
β=> SELECT * FROM t;
       s
---------------
 Hello, world!
 Another row
(2 rows)

All rows were restored. Since all available WAL records are applied by default, the server immediately switched to normal mode and is ready to accept write queries:

β=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 f
(1 row)

6. Recovery from Base Backup (immediate)

student$ sudo pg_ctlcluster 16 beta stop
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ mkdir /var/lib/postgresql/16/beta
postgres$ chmod 700 /var/lib/postgresql/16/beta
student$ sudo tar xzf /home/student/tmp/backup/base.tar.gz -C /var/lib/postgresql/16/beta

Create recovery.signal, and add the restore command and recovery target to the configuration file:

postgres$ touch /var/lib/postgresql/16/beta/recovery.signal
postgres$ cat << EOF | tee /var/lib/postgresql/16/beta/postgresql.auto.conf
restore_command = 'cp /var/lib/postgresql/archive/%f %p'
recovery_target = 'immediate'
EOF
restore_command = 'cp /var/lib/postgresql/archive/%f %p'
recovery_target = 'immediate'

Start the server.

student$ sudo pg_ctlcluster 16 beta start
student$ psql -p 5433 -d backup_archive
β=> SELECT * FROM t;
       s
---------------
 Hello, world!
(1 row)

Only the first row was restored. This time, only the WAL records required to bring the data to a consistent state were applied. The server is already accepting read queries, and recovery can be continued if needed:

β=> SELECT pg_is_in_recovery(), pg_get_wal_replay_pause_state();

Page 26: Page 26

Source slide page 26: Page 26{#source-figure-026 caption=“Source slide page 26: Page 26”}

 pg_is_in_recovery | pg_get_wal_replay_pause_state
-------------------+-------------------------------
 t                 | paused
(1 row)

The pg_get_wal_replay_pause_state() function returns: not paused – no pause was requested pause requested – a pause has been requested paused – recovery is paused Since we don’t need to apply any further records, we complete the recovery by promoting the server to normal mode (superuser privileges required):

β=> SELECT pg_wal_replay_resume();
 pg_wal_replay_resume
----------------------
(1 row)
β=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 f
(1 row)

To avoid manually promoting the server out of recovery mode, we could have set recovery_target_action = promote in advance.

Cleanup

Remove the backup and databases, reset the configuration.

student$ rm -rf /home/student/tmp/backup
β=> ALTER SYSTEM RESET archive_mode;
ALTER SYSTEM
β=> ALTER SYSTEM RESET archive_command;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta stop
α=> \c student postgres
You are now connected to database "student" as user "postgres".
α=> DROP DATABASE backup_archive;
DROP DATABASE
α=> ALTER SYSTEM RESET archive_mode;
ALTER SYSTEM
α=> ALTER SYSTEM RESET archive_command;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart
postgres$ rm -rf /var/lib/postgresql/archive

Page 27: Practice+

1. On the alpha server, create a table in which data will be periodically written. Create a physical backup and configure continuous WAL archiving. 2. Insert 100 rows into the prepared table three times (data for January, February and March), marking the time immediately after insertion. Delete some lines between inserts: simulate a situation in which the user accidentally deletes part of the data at an unknown moment, but the problem is not noticed immediately. 3. Recover the deleted data without stopping the alpha server. 3. Use the beta server as an auxiliary.

Page 28: 1. Table and Backup with WAL Archive

α=> CREATE DATABASE backup_archive;
CREATE DATABASE
α=> \c backup_archive
You are now connected to database "backup_archive" as user "student".

Let’s prepare the table:

α=> CREATE TABLE t (
  id integer PRIMARY KEY,
  month text
);
CREATE TABLE

After setting up the data structure, create a physical backup:

postgres$ pg_basebackup \
  --checkpoint=fast \
  --pgdata /var/lib/postgresql/backup

WAL segment archive:

postgres$ mkdir /var/lib/postgresql/archive

Add the archiving parameters file to the supplementary configuration directory.

postgres$ cat << EOF | tee /etc/postgresql/16/alpha/conf.d/arch.conf
archive_mode = 'on'
archive_command = 'test ! -f /var/lib/postgresql/archive/%f && cp %p /var/lib/postgresql/archive/%f'
archive_timeout = '30s'
EOF
archive_mode = 'on'
archive_command = 'test ! -f /var/lib/postgresql/archive/%f && cp %p
/var/lib/postgresql/archive/%f'
archive_timeout = '30s'

Restart the server.

student$ sudo pg_ctlcluster 16 alpha restart
student$ psql -d backup_archive

2. Inserting Data into the Table

Our application inserts 100 rows into the table at the end of each month. Insert rows for January:

α=> INSERT INTO t SELECT g.i, 'January' FROM generate_series(1,100) AS g(i);
INSERT 0 100

Save the timestamp after the insert completes:

α=> SELECT now();
             now
------------------------------
 2026-09-17 00:09:53.91657+03
(1 row)

A user accidentally deletes some of the data:

α=> DELETE FROM t WHERE id BETWEEN 42 AND 111;
DELETE 59

Insert rows for February:

α=> INSERT INTO t SELECT g.i, 'February' FROM generate_series(101,200) AS g(i);
INSERT 0 100

Page 29: Page 29

Save the timestamp again after the transaction completes:

α=> SELECT now();
              now
-------------------------------
 2026-09-17 00:10:04.242256+03
(1 row)

The user runs the same command again for some reason:

α=> DELETE FROM t WHERE id BETWEEN 42 AND 111;
DELETE 11

The error goes unnoticed for now. Insert another batch of rows:

α=> INSERT INTO t SELECT g.i, 'March' FROM generate_series(201,300) AS g(i);
INSERT 0 100

Let’s record the timestamp once more:

α=> SELECT now();
              now
-------------------------------
 2026-09-17 00:10:14.540194+03
(1 row)

The error has been discovered:

α=> SELECT month, count(*) FROM t GROUP BY month;
  month   | count
----------+-------
 February |    89
 March    |   100
 January  |    41
(3 rows)

3. Recovery Using the Archive

Let’s try to recover the data without stopping the alpha server. Since the January and February data is missing, the recovery plan is as follows: Restore the beta server from the WAL archive to the point when the January rows finished committing Export a logical backup of the January data Restore the beta server again, this time to the point when the February rows finished committing Export a logical backup of the February data (the January data may be corrupted at this point) Load the recovered data into the application table on the alpha server Stop beta and place the physical backup into its data directory.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ mv /var/lib/postgresql/backup /var/lib/postgresql/16/beta

We will need a recovery.signal file:

postgres$ touch /var/lib/postgresql/16/beta/recovery.signal

The plan is to restore the server to a specific point in time – that is, perform point-in-time recovery (PITR). Set the restore command and specify the recovery target as the timestamp created right after the January data finished committing:

postgres$ cat << EOF | tee /etc/postgresql/16/beta/conf.d/recovery.conf
restore_command = 'cp /var/lib/postgresql/archive/%f %p'
recovery_target_time = '2026-09-17 00:09:53.91657+03'
EOF
restore_command = 'cp /var/lib/postgresql/archive/%f %p'
recovery_target_time = '2026-09-17 00:09:53.91657+03'

Before starting recovery, force a segment switch on alpha to ensure all WAL records make it into the archive. Archiving takes place when writing to the next segment begins.

Page 30: Page 30

α=> SELECT pg_switch_wal();
 pg_switch_wal
---------------
 0/301D308
(1 row)
α=> CHECKPOINT;
CHECKPOINT

Start the beta server.

student$ sudo pg_ctlcluster 16 beta start
student$ psql -p 5433 -d backup_archive

Verify that the January data has been restored:

β=> SELECT month, count(*) FROM t GROUP BY month;
  month  | count
---------+-------
 January |   100
(1 row)

The beta server is in recovery mode:

β=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 t
(1 row)

This doesn’t prevent us from exporting a logical backup.

postgres$ mkdir /var/lib/postgresql/tmp
β=> COPY (SELECT * FROM t WHERE month = 'January') TO '/var/lib/postgresql/tmp/jan.dump';
COPY 100

Now let’s move on to February. Set a new target time:

postgres$ sed -i "/recovery_target_time/c\recovery_target_time = '2026-09-17 00:10:04.242256+03'"
/etc/postgresql/16/beta/conf.d/recovery.conf
postgres$ cat /etc/postgresql/16/beta/conf.d/recovery.conf
restore_command = 'cp /var/lib/postgresql/archive/%f %p'
recovery_target_time = '2026-09-17 00:10:04.242256+03'

Restart beta to continue recovery.

student$ sudo pg_ctlcluster 16 beta restart
student$ psql -p 5433 -d backup_archive

Verify the recovered data:

β=> SELECT month, count(*) FROM t GROUP BY month;
  month   | count
----------+-------
 February |   100
 January  |    41
(2 rows)

The February rows are where they should be, but the January rows are already corrupted. Export the February data:

β=> COPY (SELECT * FROM t WHERE month = 'February') TO '/var/lib/postgresql/tmp/feb.dump';
COPY 100

Restore the exported data for both months on alpha.

α=> DELETE FROM t WHERE month IN ('January', 'February');
DELETE 130
α=> COPY t FROM '/var/lib/postgresql/tmp/jan.dump';

Page 31: Page 31

COPY 100
α=> COPY t FROM '/var/lib/postgresql/tmp/feb.dump';
COPY 100

It worked:

α=> SELECT month, count(*) FROM t GROUP BY month;
  month   | count
----------+-------
 February |   100
 March    |   100
 January  |   100
(3 rows)

In PostgreSQL 17 and later, you can skip the delete step and use the COPY command with the ON ERROR ignore parameter to quietly skip rows that already exist in the table.

Cleanup

Remove the databases and configuration changes.

student$ sudo pg_ctlcluster 16 beta stop
α=> \c postgres
You are now connected to database "postgres" as user "student".
α=> DROP DATABASE backup_archive;
DROP DATABASE
postgres$ rm /etc/postgresql/16/beta/conf.d/recovery.conf
student$ sudo pg_ctlcluster 16 alpha restart

Remove any unnecessary files.

postgres$ rm -rf /var/lib/postgresql/{tmp,archive,backup}

04 Lesson 04: Replication / Physical Replication

Native Markdown source page: Replication / Physical Replication
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: No matching course-page link was captured in the source inventory. Archive: DBA3-handouts-16-en-20260917.zip PDF pages: 39

Page 1: Replication / Physical Replication

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Alexey Beresnev Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Replication Use Cases
  • How Physical Replication Works
  • WAL Record Delivery
  • Limitations and Considerations
  • Synchronous and Asynchronous Replication
  • Replication Monitoring
  • Common Issues and Troubleshooting

Page 3: Replication Use Cases

Replication

  • the process of synchronizing multiple copies of a database cluster on
  • different servers
  • A fundamental mechanism serving multiple goals
    • reliability
    • when a failure occurs,
    • the system must remain operational
    • scalability
    • load balancing across servers

Speaker notes

A single server managing databases may not meet the requirements of the system. There are two main challenges that require multiple servers to address.

First, reliability: one physical server is a possible point of failure. If the server fails, the system becomes unavailable.

Secondly, performance. One server may not be able to handle the load. Often, it’s preferable to scale horizontally rather than vertically, distributing the load across multiple servers. This can be due to both hardware costs and the need for different configurations tailored to different workload types (such as OLTP and reporting).

In PostgreSQL, distributed systems follow a “shared nothing” architecture: multiple servers operate independently without sharing memory or storage. Therefore, when servers need to work with the same data, that data must be kept synchronized between them. This synchronization mechanism is called replication.

Page 4: Leader→Replica

WAL entries are transmitted to the replica and applied there

  • data flows in one direction only
  • the entire cluster is replicated; selective replication is not supported
  • The replica is an exact copy of the leader
    • the same major server version
    • fully compatible architectures and platforms
  • Replica is read-only

Speaker notes

During physical replication, servers have assigned roles: one is the leader and others are replicas.

The leader sends write-ahead log records to the replica, which then applies them to its data files. The replica does not “understand” the changes, just applies them mechanically without any interpretation of what the changes mean, but it is faster than re-executing the original SQL statements. However, binary compatibility between servers is critical: they must run on the same hardware and software and use matching major versions of PostgreSQL.

https://postgrespro.com/docs/postgresql/16/hot-standby#HOT-STANDBY-AD MIN

Since the WAL is shared across the entire cluster, only the cluster as a whole can be replicated. There’s no way to filter which log entries get replicated.

Replica does not generate records, just applies them. Therefore, replicas are read-only: no data modification operations allowed.

Page 5: Streaming Replication

Source slide page 5: Streaming Replication{#source-figure-005 caption=“Source slide page 5: Streaming Replication”}

Speaker notes

If there is a recovery.signal file, the server is restored from the backup: replays the WAL records, restores consistency and completes the recovery. Starting the replica is similar, but the recovery does not complete: the replica continues to read and apply records from the leader. The signal for this is the standby.signal file.

In the topic “Base Backup,” we considered two ways to deliver WAL records from the server to the client: streaming and file-based. Replication uses the same two mechanisms. Streaming replication is the most common approach by far.

The wal receiver process on the replica connects to the leader over the replication protocol, using the connection string from the primary_conninfo parameter, and reads the stream of WAL records. With streaming replication, the replica lag is minimal and can be reduced to zero if needed. The received records are written to disk as WAL segments, just as they are on the leader. Next, the startup process – the same one that handles crash recovery – applies records to data files, only now it works continuously.

The leader starts a wal sender process to handle this connection. Streaming replication can use a replication slot when necessary. The slot name is stored in the primary_slot_name parameter.

Page 6: Streaming Replication Configuration

Since our configuration will not include a WAL archive, it is essential to use a replication slot at every stage – otherwise, given sufficient delay, the primary may delete the required file segments before they are committed, forcing you to take a new backup and redo the entire setup. Create a slot:

α=> SELECT pg_create_physical_replication_slot('replica');
 pg_create_physical_replication_slot
-------------------------------------
 (replica,)
(1 row)

Examine the slot we just created:

α=> SELECT * FROM pg_replication_slots \gx
-[ RECORD 1 ]-------+---------
slot_name           | replica
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         |
confirmed_flush_lsn |
wal_status          |
safe_wal_size       |
two_phase           | f
conflicting         |

The slot is not initialized right away (restart_lsn and wal_status are empty). Take a standalone backup into the beta server’s data directory using the slot we created. When using the -R option, the tool adds the files needed to start the future replica to the backup.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ pg_basebackup \
  --checkpoint=fast \
  --pgdata=/var/lib/postgresql/16/beta \
  -R --slot=replica

Check the slot again:

α=> SELECT * FROM pg_replication_slots \gx
-[ RECORD 1 ]-------+----------
slot_name           | replica
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 0/2000000
confirmed_flush_lsn |
wal_status          | reserved
safe_wal_size       |
two_phase           | f
conflicting         |

Page 7: Page 7

Once the backup completes, the slot is initialized and the primary retains all WAL files starting from the beginning of the backup (restart_lsn, wal_status). Compare with the line in backup_label:

postgres$ head -n 1 /var/lib/postgresql/16/beta/backup_label
START WAL LOCATION: 0/2000028 (file 000000010000000000000002)

The postgresql.auto.conf file was generated by pg_basebackup because of the -R option. This file contains the connection information for the primary and the replication slot name:

postgres$ cat /var/lib/postgresql/16/beta/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
primary_conninfo = 'user=postgres passfile=''/var/lib/postgresql/.pgpass''
channel_binding=prefer host=''/var/run/postgresql'' port=5432 sslmode=prefer
sslnegotiation=postgres sslcompression=0 sslcertmode=allow sslsni=1
ssl_min_protocol_version=TLSv1.2 gssencmode=prefer krbsrvname=postgres gssdelegation=0
target_session_attrs=any load_balance_hosts=disable'
primary_slot_name = 'replica'

By default, the replica will be a hot standby, meaning it can serve queries during recovery. If this is not necessary, the replica can be warm instead (hot_standby = off). The tool also created the standby.signal file. Its presence instructs the server to enter continuous recovery mode.

postgres$ ls -l /var/lib/postgresql/16/beta/standby.signal
-rw------- 1 postgres postgres 0 Sep 16 23:57 /var/lib/postgresql/16/beta/standby.signal

The WAL records needed to bring the replica to a consistent state will be read from the standalone base backup. Then it will enter continuous recovery mode and begin receiving and replaying the WAL stream.

student$ sudo pg_ctlcluster 16 beta start

Page 8: Replication with a WAL archive

Source slide page 8: Replication with a WAL archive{#source-figure-008 caption=“Source slide page 8: Replication with a WAL archive”}

Speaker notes

Another way to deliver WAL records is to give the replica access to the WAL archive, just as you would during regular recovery from archive (this mechanism is covered in the “Backup” module).

If the replica can’t retrieve the next WAL record via the replication protocol for any reason (such as a connection loss), it will attempt to read it from the archive using the command specified in the restore_command parameter. When the connection is restored, the replica will automatically switch back to streaming replication.

In fact, replication can work with just the archive, without streaming replication. But in this case:

● the replica is forced to lag behind the leader by the time it takes to fill the WAL segment;

● the leader has no awareness of the replica’s existence, which can lead to problems in certain situations.

Page 9: Processes

Leader Replica

  • wal sender
  • wal receiver
  • startup
  • archiver
  • archiver
  • (archive_mode = always)
  • checkpointer
  • checkpointer
  • (checkpoints)
  • (restartpoints)
  • background writer
  • background writer
  • wal writer
  • autovacuum

Speaker notes

Different sets of processes run on the leader and on the replica. Beyond the wal sender (on the leader) and wal receiver and startup processes (on the replica) we’ve already covered, there are others.

Common processes:

● background writer – writes dirty pages from the buffer cache to disk (on the replica, pages are read into the buffer cache in the same way to apply WAL records, then written either in the background or during page eviction).

● checkpointer – performs restartpoints (See “WAL Archive”). If a failure occurs during recovery, the replica can resume from the last restartpoint rather than starting over from the beginning.

Processes running only on the leader:

● wal writer – the replica doesn’t generate WAL records; it only receives them from the leader.

● autovacuum launcher/worker — the replica doesn’t perform vacuum operations; when vacuum runs on the primary, it’s propagated to the replica through WAL.

There is also a process that may or may not be present depending on the configuration:

● archiver — runs only on the leader when archive_mode = on, but runs on both the leader and replica when archive_mode = always (covered in more detail in the “Switchover to Replica” module).

Page 10: Replica Processes

Compare the processes on the primary:

student$ ps f -C postgres -o pid,command | grep alpha
  73709 /usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/alpha -c
config_file=/etc/postgresql/16/alpha/postgresql.conf
  73710  \_ postgres: 16/alpha: checkpointer
  73711  \_ postgres: 16/alpha: background writer
  73713  \_ postgres: 16/alpha: walwriter
  73714  \_ postgres: 16/alpha: autovacuum launcher
  73715  \_ postgres: 16/alpha: logical replication launcher
  73758  \_ postgres: 16/alpha: student student [local] idle
  74265  \_ postgres: 16/alpha: walsender postgres [local] streaming 0/3000060

And on the replica:

student$ ps f -C postgres -o pid,command | grep beta
  74260 /usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/beta -c
config_file=/etc/postgresql/16/beta/postgresql.conf
  74261  \_ postgres: 16/beta: checkpointer
  74262  \_ postgres: 16/beta: background writer
  74263  \_ postgres: 16/beta: startup recovering 000000010000000000000003
  74264  \_ postgres: 16/beta: walreceiver streaming 0/3000060

Page 11: Archive Cleanup

Source slide page 11: Archive Cleanup{#source-figure-011 caption=“Source slide page 11: Archive Cleanup”}

Speaker notes

At restartpoints, the checkpointer process will delete unneeded WAL segments from the local pg_wal directory.

If the archive is used to synchronize just one replica, you can also clean it automatically. The archive_cleanup_command parameter specifies the command that will be executed at the end of each restartpoint. For example, you can set it to run the pg_archivecleanup utility (the server will set the restartpoint segment name as %r):

archive_cleanup_command = ‘pg_archivecleanup /path/to/archive %r’

https://postgrespro.com/docs/postgresql/16/runtime-config-wal#RUNTIME-C ONFIG-WAL-ARCHIVE-RECOVERY

https://postgrespro.com/docs/postgresql/16/pgarchivecleanup

Page 12: Replica usage

Allowed

  • read-only queries (select, copy to, cursors)
  • setting server parameters (set, reset)
  • transaction management (begin, commit, rollback…)
  • creating a backup (pg_basebackup)
  • Not allowed
    • any changes (insert, update, delete, truncate, nextval…)
    • locks expecting changes (select for update…)
    • DDL commands (create, drop…), including creating temporary tables
    • maintenance commands (vacuum, analyze, reindex…)
    • access control (grant, revoke…)

Speaker notes

We’ve already mentioned that replicas don’t allow any data modifications. More specifically, the following operations are not allowed:

● modifications to tables, materialized views, and sequences;

● locks (since these require modifying data pages);

● DDL commands, including creating temporary tables;

● commands such as VACUUM and ANALYZE;

● access control commands.

A replica can execute read queries when hot_standby = on. Setting server parameters and transaction management commands will also work. For example, you can start a (reading) transaction with a specific isolation level.

Additionally, replicas can be used for creating backups.

Page 13: Replica usage

Run several commands on the primary server:

α=> CREATE DATABASE replica_physical;
CREATE DATABASE
α=> \c replica_physical
You are now connected to database "replica_physical" as user "student".
α=> CREATE TABLE test(s text);
CREATE TABLE
α=> INSERT INTO test VALUES ('Hello, world!');
INSERT 0 1

Check the replica:

student$ psql -p 5433
β=> \c replica_physical
You are now connected to database "replica_physical" as user "student".
β=> SELECT * FROM test;
       s
---------------
 Hello, world!
(1 row)

No changes can be done on replica directly:

β=> INSERT INTO test VALUES ('Replica');
ERROR:  cannot execute INSERT in a read-only transaction

In general, you can tell a replica apart from the primary using the function:

β=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 t
(1 row)

Page 14: Limitations

Isolation and MVCC

  • large number of savepoints may slow down query execution
  • Serializable isolation level is not supported
  • Triggers and locks
    • triggers and advisory locks do not work
  • Creating backups on a replica
    • the full_page_writes parameter must be enabled on the leader beforehand
    • delays caused by waiting for checkpoints and segment switches
  • Order of changes
    • tablespace directories – create on replica first
    • max_… parameter values – increase on replica first

Speaker notes

Replication has several limitations and considerations.

If the leader is running a transaction with more than 64 nested transactions (that is, it is actively using savepoints), snapshot creation on the replica will be suspended, preventing query execution for a while.

The Serializable isolation level is not supported on replicas.

Triggers won’t fire on the replica since they’ve already executed on the leader: only their results get replicated. Advisory locks won’t work either.

When performing backups from a replica, there’s no way to fully control the primary server – specifically, you can’t trigger checkpoints or enable the full_page_writes parameter. Therefore, you need to enable this parameter on the leader in advance, and then wait for a checkpoint to occur, which can significantly delay the process.

Tablespace creation is replicated, but if the OS user who started PostgreSQL does not have the privileges to create the required directory, an error will occur. The best approach is to first create the directories on the leader and all replicas, then execute the CREATE TABLESPACE command.

If you increase a resource allocation parameter on the leader, replication may stop. This affects max_connections, max_prepared_transactions, max_locks_per_transaction, max_wal_senders and max_worker_processes.

Page 15: Monitoring

Source slide page 15: Monitoring{#source-figure-015 caption=“Source slide page 15: Monitoring”}

Speaker notes

During replication, issues may arise that should be caught by properly configured monitoring. The key data is WAL positions. There are five main steps:

  1. record created on leader 2) record transmitted by wal sender 3) record received by wal receiver 4) record synchronised to disk (flush) 5) record applied by startup process

The leader usually tracks all five steps. The first one is tracked through the pg_current_wal_lsn function, while the others come from the pg_stat_replication view. The replica passes replication status to the leader each time it writes to disk, but at least once every wal_receiver_status_interval (default is 10 seconds).

Information about the replication slot can be found in the pg_replication_slots view.

Some of the replication progress information can also be obtained from the replica: in the pg_stat_wal_receiver view and via pg_last_wal_receive_lsn and pg_last_wal_replay_lsn functions.

Both functions pg_is_in_recovery and pg_get_wal_replay_pause_state return replication status information. The log_replication_commands parameter enables logging of replication protocol commands to the server log.

Page 16: Replication Monitoring

The replica is currently in recovery mode and applying WAL records:

β=> SELECT pg_is_in_recovery(), pg_get_wal_replay_pause_state();
 pg_is_in_recovery | pg_get_wal_replay_pause_state
-------------------+-------------------------------
 t                 | not paused
(1 row)

The current max_connections value on the primary:

α=> SHOW max_connections;
 max_connections
-----------------
 100
(1 row)

If the number of users has grown, we’ll want to increase this limit:

α=> ALTER SYSTEM SET max_connections = 150;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

How’s the replica doing?

β=> SELECT pg_is_in_recovery(), pg_get_wal_replay_pause_state();
 pg_is_in_recovery | pg_get_wal_replay_pause_state
-------------------+-------------------------------
 t                 | paused
(1 row)

Replication has stopped. The reason is in the log:

student$ tail -n 7 /var/log/postgresql/postgresql-16-beta.log
2026-09-16 23:58:21.189 MSK [74263] WARNING:  hot standby is not possible because of
insufficient parameter settings
2026-09-16 23:58:21.189 MSK [74263] DETAIL:  max_connections = 100 is a lower setting
than on the primary server, where its value was 150.
2026-09-16 23:58:21.189 MSK [74263] CONTEXT:  WAL redo at 0/3432EE8 for
XLOG/PARAMETER_CHANGE: max_connections=150 max_worker_processes=8 max_wal_senders=10
max_prepared_xacts=0 max_locks_per_xact=64 wal_level=replica wal_log_hints=off
track_commit_timestamp=off
2026-09-16 23:58:21.189 MSK [74263] LOG:  recovery has paused
2026-09-16 23:58:21.189 MSK [74263] DETAIL:  If recovery is unpaused, the server will
shut down.
2026-09-16 23:58:21.189 MSK [74263] HINT:  You can then restart the server after making
the necessary configuration changes.
2026-09-16 23:58:21.189 MSK [74263] CONTEXT:  WAL redo at 0/3432EE8 for
XLOG/PARAMETER_CHANGE: max_connections=150 max_worker_processes=8 max_wal_senders=10
max_prepared_xacts=0 max_locks_per_xact=64 wal_level=replica wal_log_hints=off
track_commit_timestamp=off

Follow the recommendation and increase the value on the replica.

β=> ALTER SYSTEM SET max_connections = 150;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta restart

Check the replica:

student$ psql -p 5433 -d replica_physical
β=> SELECT pg_is_in_recovery(), pg_get_wal_replay_pause_state();

Page 17: Page 17

 pg_is_in_recovery | pg_get_wal_replay_pause_state
-------------------+-------------------------------
 t                 | not paused
(1 row)

Replication continues. To avoid replication stopping, limits should be increased on the replica first. The state of the replication can be checked on the primary server: To access this information, a user must be a member of the pg_read_all_stats group role or a superuser.

student$ psql -d replica_physical
α=> \drg student|pg_monitor
                      List of role grants
 Role name  |       Member of        |   Options    | Grantor
------------+------------------------+--------------+----------
 pg_monitor | pg_read_all_settings   | INHERIT, SET | postgres
 pg_monitor | pg_read_all_stats      | INHERIT, SET | postgres
 pg_monitor | pg_stat_scan_tables    | INHERIT, SET | postgres
 student    | pg_checkpoint          | INHERIT, SET | postgres
 student    | pg_create_subscription | INHERIT, SET | postgres
 student    | pg_monitor             | INHERIT, SET | postgres
 student    | pg_read_server_files   | INHERIT, SET | postgres
 student    | pg_write_server_files  | INHERIT, SET | postgres
(8 rows)

The pg_monitor role is a member of the pg_read_all_stats role and inherits its privileges.

α=> SELECT * FROM pg_stat_replication \gx
-[ RECORD 1 ]----+------------------------------
pid              | 75323
usesysid         | 10
usename          | postgres
application_name | 16/beta
client_addr      |
client_hostname  |
client_port      | -1
backend_start    | 2026-09-16 23:58:27.895802+03
backend_xmin     |
state            | streaming
sent_lsn         | 0/3437440
write_lsn        | 0/3437440
flush_lsn        | 0/3437440
replay_lsn       | 0/3437440
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async
reply_time       | 2026-09-16 23:58:30.809823+03
Note

Note Note the *_lsn and *_lag fields – they show the replica’s lag at different stages. All positions are currently in sync, with zero lag. Now let’s insert a large number of rows into the table to observe replication in action.

α=> INSERT INTO test SELECT 'Just a line' FROM generate_series(1,1_000_000);
INSERT 0 1000000
α=> SELECT *, pg_current_wal_lsn() from pg_stat_replication \gx

Page 18: Page 18

-[ RECORD 1 ]------+------------------------------
pid                | 75323
usesysid           | 10
usename            | postgres
application_name   | 16/beta
client_addr        |
client_hostname    |
client_port        | -1
backend_start      | 2026-09-16 23:58:27.895802+03
backend_xmin       |
state              | streaming
sent_lsn           | 0/7640000
write_lsn          | 0/75E0000
flush_lsn          | 0/75E0000
replay_lsn         | 0/75806F8
write_lag          | 00:00:00.098318
flush_lag          | 00:00:00.098318
replay_lag         | 00:00:00.196214
sync_priority      | 0
sync_state         | async
reply_time         | 2026-09-16 23:58:35.839864+03
pg_current_wal_lsn | 0/79151A8

A small lag has appeared. Check the replica:

β=> SELECT count(*) FROM test;
  count
---------
 1000001
(1 row)

All rows have been delivered successfully. Check the replication status one more time:

α=> SELECT *, pg_current_wal_lsn() from pg_stat_replication \gx
-[ RECORD 1 ]------+------------------------------
pid                | 75323
usesysid           | 10
usename            | postgres
application_name   | 16/beta
client_addr        |
client_hostname    |
client_port        | -1
backend_start      | 2026-09-16 23:58:27.895802+03
backend_xmin       |
state              | streaming
sent_lsn           | 0/79171B8
write_lsn          | 0/79171B8
flush_lsn          | 0/79171B8
replay_lsn         | 0/79171B8
write_lag          | 00:00:00.000609
flush_lag          | 00:00:00.008241
replay_lag         | 00:00:00.009082
sync_priority      | 0
sync_state         | async
reply_time         | 2026-09-16 23:58:36.077635+03
pg_current_wal_lsn | 0/79171B8

All positions are now in sync.

Page 19: Replication Modes

Asynchronous

  • synchronous_commit
  • = off
  • local
  • Synchronous
    • commit with callback from the synchronous replica
    • provides reliability but not consistency
    • synchronous_commit
    • = remote_write
    • = on
    • remote_apply
    • synchronous_standby_names =
    • replica list
    • FIRST N (replica list)
    • ANY N (replica list)

Speaker notes

There are two transaction commit modes. The faster asynchronous mode carries a risk of losing already committed changes if a crash occurs. In synchronous mode, the COMMIT command doesn’t complete until the record reaches disk. If you have multiple servers, you can still improve reliability by waiting for the replica to confirm that it has commited a record. In this case, the data will not be lost even if the disk fails.

Synchronous replication is enabled using the same synchronous_commit parameter that controls synchronous commits. Different values of this parameter let you control the level of reliability. With remote_write, the leader waits for confirmation that the replica has received the record (though there’s still a chance of losing changes if the replica crashes before writing the data to disk).

With on, the leader waits for confirmation that the record has been written to the replica’s disk (this is the reliable mode; however, an application querying the replica might not yet see the changes).

Finally, the remote_apply value makes the primary wait until the replica has actually applied the record. Each next mode introduces progressively longer delays.

The leader can synchronize with one or multiple replicas. You can specify a list of replicas in priority order or set up quorum-based synchronization, where the leader waits for confirmation from any N of the available replicas.

Page 20: Segment Loss

The leader might delete a WAL file that the replica still needs

  • if the replica didn’t collect it yet
  • (i.e. because it was down for a while)
  • Solution 1. Replication slot
    • makes the leader depend on the replica, requires monitoring
    • max_slot_wal_keep_size = −1
  • Solution 2. WAL archive
    • no slot required

Speaker notes

Several challenges can arise during replication, each with different possible solutions.

PostgreSQL periodically removes WAL files that are no longer needed for recovery. The leader might delete a file containing data that hasn’t yet been transmitted to the replica.

This isn’t an issue when a write-ahead log archive is available, since the replica can simply read the required data from the archive if streaming replication fails, then switch back to receiving data from the stream once it catches up with the leader.

Without the archive, the replica won’t be able to continue recovery and will need to be reconstructed from a backup. To prevent this, you can use a replication slot. However, this creates a dependency. If the replica stops collecting WAL records, they will accumulate on the leader, potentially flooding the file system. To prevent this, you can limit the accumulated volume by setting the max_slot_wal_keep_size. If this limit is exceeded, the slot will stop, and the server will be able to delete the WAL files again.

In any case, slots are yet another item you will have to monitor.

Page 21: Impact of the Replication Slot

Stop the replica.

student$ sudo pg_ctlcluster 16 beta stop

Limit the WAL size to two segments.

α=> ALTER SYSTEM SET min_wal_size = '32MB';
ALTER SYSTEM
α=> ALTER SYSTEM SET max_wal_size = '32MB';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha reload

The slot is inactive, but it retains the LSN of the last record received by the replica:

α=> SELECT active, restart_lsn, wal_status FROM pg_replication_slots \gx
-[ RECORD 1 ]----------
active      | f
restart_lsn | 0/79171B8
wal_status  | reserved

Let’s insert more rows into the table.

α=> INSERT INTO test SELECT 'Just a line' FROM generate_series(1,1_000_000);
INSERT 0 1000000

The LSN in the slot has not changed:

α=> SELECT active, restart_lsn, wal_status FROM pg_replication_slots \gx
-[ RECORD 1 ]----------
active      | f
restart_lsn | 0/79171B8
wal_status  | extended

Create a checkpoint, which should clean up the WAL since its size exceeds the allowed limit.

α=> CHECKPOINT;
CHECKPOINT

What is the WAL size now?

α=> SELECT pg_size_pretty(sum(size)) FROM pg_ls_waldir();
 pg_size_pretty
----------------
 96 MB
(1 row)

The checkpoint did not remove the segments – they are being retained by the slot, even though the size limit has been exceeded. If a slot is left unattended, it can exhaust all available disk space. In most cases, keeping the main server running smoothly is more important than keeping the replica in sync. To prevent WAL from growing out of control, you can limit the amount retained by the slot:

α=> ALTER SYSTEM SET max_slot_wal_keep_size = '16MB';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha reload
α=> CHECKPOINT;
CHECKPOINT

Now the checkpoint cleans up the WAL:

α=> SELECT pg_size_pretty(sum(size)) FROM pg_ls_waldir();

Page 22: Page 22

 pg_size_pretty
----------------
 32 MB
(1 row)

But the slot no longer guarantees the availability of WAL records (wal_status=lost):

α=> SELECT active, restart_lsn, wal_status FROM pg_replication_slots \gx
-[ RECORD 1 ]-----
active      | f
restart_lsn |
wal_status  | lost

And the replica cannot synchronize:

student$ sudo pg_ctlcluster 16 beta start
student$ sudo tail -n 2 /var/log/postgresql/postgresql-16-beta.log
2026-09-16 23:58:46.203 MSK [76622] FATAL:  could not receive data from WAL stream:
ERROR:  requested WAL segment 000000010000000000000007 has already been removed
2026-09-16 23:58:46.204 MSK [76619] LOG:  waiting for WAL to become available at 0/7002000

In such cases, you can synchronize the replica using the archive by setting the restore_command parameter. If that’s not an option, you’ll need to reconfigure replication from scratch by creating a new base backup.

Page 23: Conflicts: a Problem

WAL records incompatible with queries on the replica

  • vacuum removes tuples that are still needed by queries on the replica
  • exclusive locks on objects
  • buffer pinning
  • Monitoring
    • pg_stat_database_conflicts

Speaker notes

Another problem occurs when when the replica is used to run queries. The leader can perform an action that conflicts with a query running on the replica:

● Vacuum may remove a tuple that’s being queried, or it may need to acquire a lock that conflicts with the query.

● A WAL record may aquire an exclusive lock on an object (for example, when dropping a table).

● Buffer pinning may cause conflicts sometimes.

You can monitor such conflicts using the pg_stat_database_conflicts view.

Page 24: Conflicts: Solutions

Solution 0: Avoid conflicts

  • avoid running commands that acquire locks
  • disable file truncation at end of vacuuming: retention setting
  • vacuum_truncate = off
  • Solution 1: Delay applying conflicting records
    • max_standby_streaming_delay
    • max_standby_archive_delay
    • except deadlocks, DROP TABLESPACE, DROP DATABASE

Speaker notes

One way to handle conflicts is to avoid operations on the leader that would result in exclusive locks. For example, you can avoid running commands like DROP TABLE, TRUNCATE, LOCK, DROP INDEX, DROP TRIGGER, and similar operations, or run them only when you know there are no active queries on the replica.

Some implicit locks can be avoided by disabling the corresponding functionality on the leader. For instance, if vacuum frees up pages at the end of a file, shrinking the file size requires an exclusive lock on the table. Then, you can disable file truncation by setting the table storage parameter vacuum_truncate = off. However, vacuum cannot be disabled or postponed indefinitely, so this approach won’t help avoid conflicts when vacuum removes tuples that are still needed by queries.

Another approach is to delay the application of conflicting records on the replica. This method works with both streaming replication and archive-based replication; it delays the application of records related to both vacuum operations and exclusive locks. You can set the maximum application delay (measured from when the record is received). If the conflicting query hasn’t finished by that time, it will be terminated.

However, conflicts caused by rare causes (database or tablespace deletion, deadlocks) are not delayed and are resolved immediately by terminating the query.

Page 25: Conflicts: Solutions

Solution 2: feedback via the replication protocol

  • a snapshot on the replica will prevent tuple removal
  • in the same manner as
  • hot_standby_feedback = on
  • only for vacuum conflicts with queries on the replica
  • Monitoring
    • pg_stat_replication.backend_xmin
    • pg_replication_slots.xmin

Speaker notes

Another approach is to defer conflicting operations on the leader. To enable this, enable replication protocol feedback.

This method only works for vacuum operations (exclusive locks cannot be deferred) and only with streaming replication (if the replica switches to retrieving records from the archive, it essentially becomes invisible to the leader).

Each data snapshot includes a transaction horizon – a transaction ID value that marks the boundary between tuples that are no longer needed and those that must be retained. The minimum horizon across all snapshots on the replica determines what can be vacuumed. This topic is covered in detail in the “Data Snapshots” section of the DBA2 course.

With feedback enabled, the leader knows which tuples the replica needs. In this case, from the vacuum perspective, a query running on the replica is treated exactly the same as a query running on the leader itself.

Feedback allows you to track the vacuum horizon on the leader side. When a replication slot is not in use, the horizon can be monitored through the pg_stat_replication.backend_xmin field, while with a slot it appears in pg_replication_slots.xmin. Keep in mind that when feedback is enabled, even an inactive slot will prevent vacuum from proceeding, which can cause bloating. The practice assignments explore this effect.

Page 26: Takeaways

The replication mechanism works by streaming WAL records to the replica and applying them there

  • primary mode: streaming replication
  • can be supplemented with WAL archiving
  • The replica is an exact copy of the leader
    • does not generate its own WAL records and operates in read-only mode
  • A complex mechanism that requires proper configuration and monitoring
    • configurations vary significantly depending on your specific requirements

Page 27: Practice

  1. Configure streaming replication between two servers in synchronous mode, with a slot, without feedback. 2. Verify that replication works as intended. Make sure that when the replica is stopped, commits on the leader are not completed. 3. Simulate a scenario where a query running on the replica gets terminated because the leader has vacuumed tuples that the query still needs. 4. Disable deferral of conflicting changes and verify that queries are canceled immediately. 5. Enable feedback and verify that vacuum on the leader doesn’t interrupt query execution. 6. Stop the replica and verify that the replication slot prevents vacuum from removing tuples. Drop the slot to allow vacuum to resume.

Speaker notes

  1. For synchronous mode, set on the leader:

● synchronous_commit = on

● synchronous_standby_names = ‘“16/beta”’

  1. Start a transaction with Repeatable Read isolation level on the replica and periodically query the table. Meanwhile, on the leader, modify that table and run vacuum.

Keep in mind that the max_standby_streaming_delay parameter defaults to 30 seconds.

  1. Set the max_standby_streaming_delay parameter to zero.

  2. Enable the hot_standby_feedback parameter on the replica. The maximum notification interval is controlled by the wal_receiver_status_interval parameter (defaults to 10 seconds).

  3. After completing the tasks, delete the database you created and reset the configuration.

Remember to disable the synchronous commit mode, otherwise a stopped replica will prevent transactions from committing. Don’t set synchronous_commit to off. Instead, set synchronous_commit = “local” or reset the synchronous_standby_names parameter.

Page 28: 1. Synchronous Physical Streaming Replication

α=> CREATE DATABASE replica_physical;
CREATE DATABASE

Create and start the replica the same way as in the demonstration.

α=> \c replica_physical
You are now connected to database "replica_physical" as user "student".

Backup:

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ pg_basebackup \
  --pgdata /var/lib/postgresql/16/beta \
  --checkpoint=fast \
  -R --create-slot --slot=replica
student$ sudo pg_ctlcluster 16 beta start

For the main server to find the replica, the cluster_name parameter has already been set in the main configuration:

student$ psql -p 5433 -c 'SHOW cluster_name;'
 cluster_name
--------------
 16/beta
(1 row)

Another option is to add the application_name attribute to the connection string:

primary_conninfo='user=student port=5432 application_name=16/beta'

Now enable synchronous replication mode on the primary.

α=> ALTER SYSTEM SET synchronous_commit = on;
ALTER SYSTEM
α=> ALTER SYSTEM SET synchronous_standby_names = '"16/beta"';
ALTER SYSTEM

If all replicas are synchronous, you can set a value that matches any name:

synchronous_standby_names = '*'

However, the primary cannot distinguish a replica from, say, the pg_receivewal utility. With this configuration, it will attempt to synchronize with any client connected via the replication protocol, which can result in commits waiting indefinitely. It’s better to list names explicitly.

student$ sudo pg_ctlcluster 16 alpha reload

2. Physical Replication Verification

α=> SELECT * FROM pg_stat_replication \gx

Page 29: Page 29

-[ RECORD 1 ]----+------------------------------
pid              | 98849
usesysid         | 10
usename          | postgres
application_name | 16/beta
client_addr      |
client_hostname  |
client_port      | -1
backend_start    | 2026-09-17 00:11:28.62954+03
backend_xmin     |
state            | streaming
sent_lsn         | 0/3000C50
write_lsn        | 0/3000C50
flush_lsn        | 0/3000C50
replay_lsn       | 0/3000C50
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 1
sync_state       | sync
reply_time       | 2026-09-17 00:11:31.677795+03

sync_state: sync means that replication is running in the synchronous mode: application_name: 16/beta – the name by which the primary finds the replica. Stop the replica.

student$ sudo pg_ctlcluster 16 beta stop
α=> BEGIN;
BEGIN
α=> CREATE TABLE test(id integer);
CREATE TABLE
α=> COMMIT;

The commit waits for the synchronous replica…

student$ sudo pg_ctlcluster 16 beta start

After the replica starts, the commit completes.

COMMIT

3. Query Cancellation Due to Vacuum on the Primary

α=> INSERT INTO test VALUES (1);
INSERT 0 1
student$ psql -p 5433 -d replica_physical

When applying a WAL record on the replica is delayed beyond deadlock_timeout due to a conflict with a running query, we want a message to be written to the server log.

β=> \dconfig deadlock_timeout
List of configuration parameters
    Parameter     | Value
------------------+-------
 deadlock_timeout | 1s
(1 row)

To do this, set the log_recovery_conflict_waits parameter.

β=> ALTER SYSTEM SET log_recovery_conflict_waits = on;
ALTER SYSTEM

Also reduce the maximum time streaming replication can be paused before a query on the replica is canceled.

β=> ALTER SYSTEM SET max_standby_streaming_delay = '9s';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta reload

Start a transaction with the Repeatable Read isolation level: the first statement will create a data snapshot that all statements in the transaction will use.

Page 30: Page 30

β=> BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN
β=> SELECT * FROM test;
 id
----
  1
(1 row)

On the primary, update the only row and run vacuum. The first tuple will be removed in the process.

α=> UPDATE test SET id = 2;
UPDATE 1
α=> VACUUM test;
VACUUM

After 5 seconds, the query on the replica still completes – the replica is delaying the application of the conflicting WAL record:

β=> SELECT * FROM test;
 id
----
  1
(1 row)

And the server log already contains a message about waiting for the conflict to be resolved:

postgres$ tail -3 /var/log/postgresql/postgresql-16-beta.log
2026-09-17 00:11:39.371 MSK [99295] LOG:  recovery still waiting after 1024.875 ms:
recovery conflict on snapshot
2026-09-17 00:11:39.371 MSK [99295] DETAIL:  Conflicting process: 99381.
2026-09-17 00:11:39.371 MSK [99295] CONTEXT:  WAL redo at 0/301B8A0 for Heap2/PRUNE:
snapshotConflictHorizon: 738, nredirected: 1, ndead: 0, nunused: 0, redirected: [1->2],
dead: [], unused: []; blkref #0: rel 1663/16409/16410, blk 0

Five seconds later, the same query is aborted – the tuple from the snapshot no longer exists.

β=> SELECT * FROM test;
FATAL:  terminating connection due to conflict with recovery
DETAIL:  User query might have needed to see row versions that must be removed.
HINT:  In a moment you should be able to reconnect to the database and repeat your
command.
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
connection to server was lost

4. Disable the Delay of Conflicting WAL Record Application

Set the delay for applying conflicting changes to zero.

student$ psql -p 5433 -d replica_physical
β=> ALTER SYSTEM SET max_standby_streaming_delay = 0;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta reload

Start a new transaction…

β=> BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN
β=> SELECT * FROM test;
 id
----
  2
(1 row)

On the primary, update the row and run vacuum…

Page 31: Page 31

Source slide page 31: Page 31{#source-figure-031 caption=“Source slide page 31: Page 31”}

α=> UPDATE test SET id = 3;
UPDATE 1
α=> VACUUM test;
VACUUM

The query on the replica is canceled immediately:

β=> SELECT * FROM test;
FATAL:  terminating connection due to conflict with recovery
DETAIL:  User query might have needed to see row versions that must be removed.
HINT:  In a moment you should be able to reconnect to the database and repeat your
command.
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
connection to server was lost

5. Feedback

Set up feedback and, to avoid long waits, a short notification interval.

student$ psql -p 5433 -d replica_physical
β=> ALTER SYSTEM SET hot_standby_feedback = on;
ALTER SYSTEM
β=> ALTER SYSTEM SET wal_receiver_status_interval = '1s';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta reload

Start a new transaction…

β=> BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN
β=> SELECT * FROM test;
 id
----
  3
(1 row)

On the primary, update the row and run vacuum…

α=> UPDATE test SET id = 4;
UPDATE 1
α=> VACUUM VERBOSE test;
INFO:  vacuuming "replica_physical.public.test"
INFO:  finished vacuuming "replica_physical.public.test": index scans: 0
pages: 0 removed, 1 remain, 1 scanned (100.00% of total)
tuples: 0 removed, 2 remain, 1 are dead but not yet removable
removable cutoff: 740, which was 1 XIDs old when operation ended
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers
removed
avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
buffer usage: 16 hits, 0 misses, 0 dirtied
WAL usage: 1 records, 0 full page images, 188 bytes
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
VACUUM

Thanks to feedback, vacuum cannot remove the old tuple (tuples: 0 removed, 2 remain, 1 are dead but not yet removable).

β=> SELECT * FROM test;
 id
----
  3
(1 row)

Page 32: Page 32

β=> COMMIT;
COMMIT
α=> VACUUM VERBOSE test;
INFO:  vacuuming "replica_physical.public.test"
INFO:  finished vacuuming "replica_physical.public.test": index scans: 0
pages: 0 removed, 1 remain, 1 scanned (100.00% of total)
tuples: 1 removed, 1 remain, 0 are dead but not yet removable
removable cutoff: 741, which was 0 XIDs old when operation ended
new relfrozenxid: 740, which is 1 XIDs ahead of previous value
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers
removed
avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
buffer usage: 17 hits, 0 misses, 0 dirtied
WAL usage: 3 records, 0 full page images, 308 bytes
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
VACUUM

Once the transaction on the replica completes, the primary can run vacuum (tuples: 1 removed, 1 remain, 0 are dead but not yet removable). The WAL records are replicated, but they won’t cause any conflicts.

6. Effect of a Replication Slot on Vacuum When the Replica Is Stopped

Cancel synchronization and stop the replica:

α=> ALTER SYSTEM RESET synchronous_standby_names;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha reload
student$ sudo pg_ctlcluster 16 beta stop

Update the table row once more:

α=> UPDATE test SET id = 5;
UPDATE 1

The slot is inactive, but it retains the minimum xmin of the replica’s data snapshots:

α=> SELECT active, xmin FROM pg_replication_slots;
 active | xmin
--------+------
 f      |  741
(1 row)

This xmin value is less than the minimum xmin of local data snapshots:

α=> SELECT min(backend_xmin::text::numeric) FROM pg_stat_activity;
 min
-----
 742
(1 row)

Therefore, the slot will hold back vacuum (tuples: 0 removed, 2 remain, 1 are dead but not yet removable).

α=> VACUUM VERBOSE test;
INFO:  vacuuming "replica_physical.public.test"
INFO:  finished vacuuming "replica_physical.public.test": index scans: 0
pages: 0 removed, 1 remain, 1 scanned (100.00% of total)
tuples: 0 removed, 2 remain, 1 are dead but not yet removable
removable cutoff: 741, which was 1 XIDs old when operation ended
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers
removed
avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
buffer usage: 16 hits, 0 misses, 0 dirtied
WAL usage: 1 records, 0 full page images, 188 bytes
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
VACUUM

Delete the slot.

α=> SELECT pg_drop_replication_slot('replica');

Page 33: Page 33

 pg_drop_replication_slot
--------------------------
(1 row)

Now vacuum will run successfully (tuples: 1 removed).

α=> VACUUM VERBOSE test;
INFO:  vacuuming "replica_physical.public.test"
INFO:  finished vacuuming "replica_physical.public.test": index scans: 0
pages: 0 removed, 1 remain, 1 scanned (100.00% of total)
tuples: 1 removed, 1 remain, 0 are dead but not yet removable
removable cutoff: 742, which was 0 XIDs old when operation ended
new relfrozenxid: 741, which is 1 XIDs ahead of previous value
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers
removed
avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
buffer usage: 17 hits, 0 misses, 0 dirtied
WAL usage: 3 records, 0 full page images, 308 bytes
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
VACUUM

Cleanup

Drop the database and reset configuration.

α=> \c postgres
You are now connected to database "postgres" as user "student".
α=> DROP DATABASE replica_physical;
DROP DATABASE
α=> ALTER SYSTEM RESET synchronous_commit;
ALTER SYSTEM
α=> ALTER SYSTEM RESET synchronous_standby_names;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

Page 34: Practice+

1. Configure streaming physical replication together with WAL archiving, using the virtual machine’s network interface for the replication stream. 2. Verify that streaming replication is working through the interface. 3. Simulate a network failure by disabling the virtual machine’s network interface. 4. Ensure that streaming replication has stopped and the replica is receiving changes from the WAL archive, but with lag. 5. Restart the virtual machine’s network interface, verifying that streaming replication has automatically recovered.

Speaker notes

  1. You can check the status of network interfaces with the command: ip a

You can check the availability of the network address with the command: ping -c1

Edit the pg_hba.conf file to allow connections to the alpha server through any network interfaces, and configure it to listen on all network interfaces.

The wal_receiver process ends if there is no network connection within the wal_receiver_timeout time. Specify a small value.

  1. Simulate network failure by turning off the network interface: sudo ip l set enp0s3 down

  2. Verify that after switching to the next WAL segment, the replica receives the changes made before switching.

  3. You can switch the network interface back on with the command: sudo ip l set enp0s3 up

After completing the exercises, drop the database you created and reset the configuration parameters to their defaults.

Page 35: 1. Physical Streaming Replication and WAL Archive

The primary virtual machine network interface is enp0s3:

student$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen
1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host noprefixroute
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group
default qlen 1000
    link/ether 08:00:27:4f:a4:65 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s3
       valid_lft 78573sec preferred_lft 78573sec
    inet6 fd17:625c:f037:2:a7c5:76e6:796c:1d04/64 scope global temporary dynamic
       valid_lft 86031sec preferred_lft 14031sec
    inet6 fd17:625c:f037:2:a00:27ff:fe4f:a465/64 scope global dynamic mngtmpaddr
       valid_lft 86031sec preferred_lft 14031sec
    inet6 fe80::a00:27ff:fe4f:a465/64 scope link
       valid_lft forever preferred_lft forever

Verify that the interface is accessible.

student$ ping -c1 10.0.2.15
PING 10.0.2.15 (10.0.2.15) 56(84) bytes of data.
64 bytes from 10.0.2.15: icmp_seq=1 ttl=64 time=0.031 ms
--- 10.0.2.15 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.031/0.031/0.031/0.000 ms

Allow connections to the alpha server from IPv4 addresses belonging to the server’s interface networks.

postgres$ sed -i.bak 's/127\.0\.0\.1\/32/samenet/' /etc/postgresql/16/alpha/pg_hba.conf

Enable the server to listen on all interfaces and configure WAL archiving.

α=> ALTER SYSTEM SET listen_addresses = '*';
ALTER SYSTEM
α=> ALTER SYSTEM SET archive_mode = on;
ALTER SYSTEM
α=> ALTER SYSTEM SET archive_command =
'test ! -f /var/lib/postgresql/archive/%f && cp %p /var/lib/postgresql/archive/%f';
ALTER SYSTEM

Directory for the WAL archive.

postgres$ mkdir /var/lib/postgresql/archive
student$ sudo pg_ctlcluster 16 alpha restart

Backup:

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ pg_basebackup --pgdata=/var/lib/postgresql/16/beta --checkpoint=fast

Prepare the configuration for the replica. The wal_receiver_timeout parameter defines how long the replica waits before terminating an idle connection. This allows the replica to detect a dropped network connection more quickly.

postgres$ cat << EOF | tee /etc/postgresql/16/beta/conf.d/rest.conf
restore_command = 'cp /var/lib/postgresql/archive/%f %p'
primary_conninfo = 'user=postgres password=postgres host=10.0.2.15 port=5432'
wal_receiver_timeout = '2s'
EOF
restore_command = 'cp /var/lib/postgresql/archive/%f %p'

Page 36: 2. Replication Check

primary_conninfo = 'user=postgres password=postgres host=10.0.2.15 port=5432'
wal_receiver_timeout = '2s'
postgres$ touch /var/lib/postgresql/16/beta/standby.signal
student$ sudo pg_ctlcluster 16 beta start
student$ psql
α=> CREATE DATABASE replica_physical;
CREATE DATABASE
α=> \c replica_physical
You are now connected to database "replica_physical" as user "student".
α=> CREATE TABLE test(s text);
CREATE TABLE
α=> INSERT INTO test VALUES ('Hello, world!');
INSERT 0 1

The replica receives WAL records via the stream…

student$ psql -p 5433
β=> SELECT pid, status, written_lsn, flushed_lsn, latest_end_lsn, sender_host
FROM pg_stat_wal_receiver \gx
-[ RECORD 1 ]--+----------
pid            | 101744
status         | streaming
written_lsn    | 0/3486380
flushed_lsn    | 0/3486380
latest_end_lsn | 0/3486380
sender_host    | 10.0.2.15

…and reports received records to the primary without delay:

α=> SELECT * FROM pg_stat_replication \gx
-[ RECORD 1 ]----+------------------------------
pid              | 101745
usesysid         | 10
usename          | postgres
application_name | 16/beta
client_addr      | 10.0.2.15
client_hostname  |
client_port      | 55376
backend_start    | 2026-09-17 00:12:33.618001+03
backend_xmin     |
state            | streaming
sent_lsn         | 0/3486380
write_lsn        | 0/3486380
flush_lsn        | 0/3486380
replay_lsn       | 0/3486380
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async
reply_time       | 2026-09-17 00:12:40.460627+03
β=> \c replica_physical
You are now connected to database "replica_physical" as user "student".

The table has been replicated:

β=> SELECT * FROM test;
       s
---------------
 Hello, world!
(1 row)

Page 37: 3. Network Failure

Disable the network interface to simulate a network failure.

student$ sudo ip l set enp0s3 down

The network is unavailable.

student$ ping -c1 10.0.2.15
ping: connect: Network is unreachable

The wal receiver process has stopped:

β=> SELECT * FROM pg_stat_wal_receiver \gx
(0 rows)

On the primary, insert a row…

α=> INSERT INTO test VALUES ('Network failure.');
INSERT 0 1

…but the replica doesn’t receive it:

β=> SELECT * FROM test;
       s
---------------
 Hello, world!
(1 row)

Streaming replication has stopped:

α=> SELECT sent_lsn, write_lsn FROM pg_stat_replication \gx
-[ RECORD 1 ]--------
sent_lsn  | 0/34863F0
write_lsn | 0/3486380

4. Archiving

Archiving on the primary, however, continues to function. Switch the WAL segment:

α=> SELECT pg_walfile_name(pg_current_wal_lsn()), pg_switch_wal();
     pg_walfile_name      | pg_switch_wal
--------------------------+---------------
 000000010000000000000003 | 0/3486458
(1 row)
α=> INSERT INTO test VALUES ('WAL segment switched.');
INSERT 0 1

The segment has been archived:

α=> SELECT * FROM pg_stat_archiver \gx
-[ RECORD 1 ]------+------------------------------
archived_count     | 4
last_archived_wal  | 000000010000000000000003
last_archived_time | 2026-09-17 00:12:50.518266+03
failed_count       | 0
last_failed_wal    |
last_failed_time   |
stats_reset        | 2026-07-15 23:46:12.428787+03

Now the replica can apply the records from the new segment, and we will see the row that was inserted before the switch.

β=> SELECT * FROM test;

Page 38: 5. Network Recovery

        s
------------------
 Hello, world!
 Network failure.
(2 rows)

Let’s insert another row into the table on the primary.

α=> INSERT INTO test VALUES ('Network still down...');
INSERT 0 1

It doesn’t reach the replica.

β=> SELECT * FROM test;
        s
------------------
 Hello, world!
 Network failure.
(2 rows)

Now restore the network interface: the outage has been resolved.

student$ sudo ip l set enp0s3 up
student$ ping -c1 10.0.2.15
PING 10.0.2.15 (10.0.2.15) 56(84) bytes of data.
64 bytes from 10.0.2.15: icmp_seq=1 ttl=64 time=0.018 ms
--- 10.0.2.15 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.018/0.018/0.018/0.000 ms

Streaming replication is working again:

α=> SELECT * FROM pg_stat_replication \gx
-[ RECORD 1 ]----+------------------------------
pid              | 103734
usesysid         | 10
usename          | postgres
application_name | 16/beta
client_addr      | 10.0.2.15
client_hostname  |
client_port      | 49664
backend_start    | 2026-09-17 00:12:59.483625+03
backend_xmin     |
state            | streaming
sent_lsn         | 0/4000150
write_lsn        | 0/4000150
flush_lsn        | 0/4000150
replay_lsn       | 0/4000150
write_lag        | 00:00:00.095116
flush_lag        | 00:00:00.102971
replay_lag       | 00:00:00.103039
sync_priority    | 0
sync_state       | async
reply_time       | 2026-09-17 00:12:59.616749+03

On the primary, add another row.

α=> INSERT INTO test VALUES ('Network restored!');
INSERT 0 1

The replica has received all the rows.

β=> SELECT * FROM test;

Page 39: Cleanup

Source slide page 39: Cleanup{#source-figure-039 caption=“Source slide page 39: Cleanup”}

           s
-----------------------
 Hello, world!
 Network failure.
 WAL segment switched.
 Network still down...
 Network restored!
(5 rows)

Drop the database we created and reset configuration.

α=> \c postgres
You are now connected to database "postgres" as user "student".
α=> DROP DATABASE replica_physical;
DROP DATABASE
β=> \c postgres
You are now connected to database "postgres" as user "student".
β=> ALTER SYSTEM RESET listen_addresses;
ALTER SYSTEM
β=> ALTER SYSTEM RESET archive_mode;
ALTER SYSTEM
β=> ALTER SYSTEM RESET archive_command;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta stop -m immediate --skip-systemctl-redirect
α=> ALTER SYSTEM RESET listen_addresses;
ALTER SYSTEM
α=> ALTER SYSTEM RESET archive_mode;
ALTER SYSTEM
α=> ALTER SYSTEM RESET archive_command;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart
postgres$ rm -f /etc/postgresql/16/beta/conf.d/rest.conf

05 Lesson 05: Replication / Switchover to Replica

Native Markdown source page: Replication / Switchover to Replica
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: No matching course-page link was captured in the source inventory. Archive: DBA3-handouts-16-en-20260917.zip PDF pages: 34

Page 1: Replication / Switchover to Replica

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Alexey Beresnev Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Switchover to Replica
  • Failback on Former Leader
  • Considerations for File-Based Archiving

Page 3: Switchover to Replica

Reasons

  • planned switchover:
  • scheduled maintenance
  • emergency switchover (failover):
  • switch to replica after a leader failure
  • Procedure
    • make sure the leader is stopped
    • $ pg_ctl promote or
    • pg_promote()
    • automatic switchover: unavailable
    • (third-party cluster management software required)

Speaker notes

You might need to switch to a standby server for maintenance work on the primary server. In this case, the switchover can be performed at a convenient time in a planned manner. In the event of a main server failure, you need to switch to the standby server as quickly as possible to minimize system downtime (failover).

A replica is promoted to independent mode by either executing the command pg_ctl promote from the OS or the function pg_promote from SQL. Switching with the trigger file (the promote_trigger_file option) is no longer supported in PostgreSQL 16 and higher.

Before promoting a replica, you must make sure that the original leader is stopped. If it continues to work and clients are still connected to it, data on different servers can disperse, and reconciling them afterwards is messy, non-automatable, and often resulting in some data being lost.

To switch to a standby server, you must redirect clients to it. This requires the use of third-party software (as discussed in the “Cluster Technologies” module), since PostgreSQL does not have its own tools for this.

https://postgrespro.com/docs/postgresql/16/warm-standby-failover

Page 4: Replica Promotion

Apply WAL records

  • already received by the wal receiver but not yet applied by the startup
  • process
  • Terminate processes
    • wal receiver
    • startup
  • Start processes
    • wal writer
    • autovacuum launcher
    • archiver (if configured)
  • Switch to a new timeline

Speaker notes

After receiving the signal, the replica gets promoted: it performs recovery and becomes a regular server.

The startup process applies log records that have already been received by the wal receiver process, but have not yet been applied. Then, the wal receiver and startup processes terminate since they are not needed on the leader.

On the other hand, the wal writer and autovacuum launcher processes are started up (archiver too, if configured.)

After completing recovery, the server switches to a new timeline.

Tiny details aside, the process is essentially just a recovery from backup.

Page 5: Streaming Replication Configuration

In this demo, we will run checkpoints manually and increase the scheduled interval.

α=> ALTER SYSTEM SET checkpoint_timeout = '1d';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha reload
α=> CREATE DATABASE replica_switchover;
CREATE DATABASE

Let’s enable the pg_walinspect extension. It will help us inspect checkpoint entries in the WAL. Superuser privileges are required.

α=> \c replica_switchover postgres
You are now connected to database "replica_switchover" as user "postgres".
α=> CREATE EXTENSION pg_walinspect;
CREATE EXTENSION
α=> \c - student
You are now connected to database "replica_switchover" as user "student".
α=> CREATE TABLE test(s text);
CREATE TABLE
α=> INSERT INTO test VALUES ('Initial state. The only server is alpha.');
INSERT 0 1

We will be using the standalone backup multiple times, so we create it in tar format.

student$ pg_basebackup \
  --format=tar \
  --compress=gzip \
  --checkpoint=fast \
  --pgdata=/home/student/tmp/backup

Let’s place the backup into the PGDATA directory on the beta server:

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ mkdir /var/lib/postgresql/16/beta
postgres$ chmod 700 /var/lib/postgresql/16/beta
student$ sudo tar xzf /home/student/tmp/backup/base.tar.gz -C /var/lib/postgresql/16/beta
student$ sudo tar xzf /home/student/tmp/backup/pg_wal.tar.gz -C /var/lib/postgresql/16/beta/pg_wal

We will need the standby.signal file.

postgres$ touch /var/lib/postgresql/16/beta/standby.signal

Configure the connection to the primary.

postgres$ cat << EOF >> /var/lib/postgresql/16/beta/postgresql.auto.conf
primary_conninfo = 'user=student port=5432'
primary_slot_name = 'replica'
EOF

Create a replication slot on alpha and start the replica beta.

student$ pg_receivewal --create-slot --slot=replica
student$ sudo pg_ctlcluster 16 beta start

Check the replica:

α=> INSERT INTO test VALUES ('Replication check: alpha – primary, beta — replica.');
INSERT 0 1

Page 6: Page 6

student$ psql -p 5433 -d replica_switchover
β=> SELECT * FROM test;
                          s
-----------------------------------------------------
 Initial state. The only server is alpha.
 Replication check: alpha – primary, beta — replica.
(2 rows)

Stopping the Primary and Creating the Checkpoint

Let’s add one more row to the table and gracefully stop the primary. A checkpoint will be performed in the process.

α=> INSERT INTO test VALUES ('Before graceful stop of primary alpha.');
INSERT 0 1
student$ sudo pg_ctlcluster 16 alpha stop

Checkpoint information has been written to the control file:

postgres$ /usr/lib/postgresql/16/bin/pg_controldata -D ~postgres/16/alpha | \
  grep -E '^.*(cluster|checkpoint.*location).*:'
Database cluster state:               shut down
Latest checkpoint location:           0/3000200
Latest checkpoint's REDO location:    0/3000200

Did the replica receive this checkpoint record? Let’s examine the WAL on the replica, starting from the Log Sequence Number of the checkpoint record.

β=> SELECT start_lsn, prev_lsn, resource_manager, record_type, record_length,
  left(description, 46) || '...' description
FROM pg_get_wal_records_info('0/3000200', 'FFFFFFFF/FFFFFFFF') \gx
-[ RECORD 1 ]----+--------------------------------------------------
start_lsn        | 0/3000200
prev_lsn         | 0/30001D8
resource_manager | XLOG
record_type      | CHECKPOINT_SHUTDOWN
record_length    | 114
description      | redo 0/3000200; tli 1; prev tli 1; fpw true; x...

The record_length field shows the record length in bytes. The last position received by the replica is greater than the LSN of the checkpoint record:

β=> SELECT pg_last_wal_receive_lsn(), pg_wal_lsn_diff(pg_last_wal_receive_lsn(), '0/3000200' );
 pg_last_wal_receive_lsn | pg_wal_lsn_diff
-------------------------+-----------------
 0/3000278               |             120
(1 row)

The difference slightly exceeds the record length (114) due to WAL record alignment: on a 64-bit platform, record LSNs are multiples of eight bytes.

Replica Promotion

The beta server is currently a replica (it is in recovery mode):

β=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 t
(1 row)

Promote the replica. You can also use the pg_promote() function, which does the same thing.

student$ sudo pg_ctlcluster 16 beta promote

There is another way to promote a replica: delete the standby.signal file and restart the standby server. This is not quite a “proper” way to do it, since the replica won’t recognize that recovery is complete and won’t switch to a new timeline.

Page 7: Page 7

The former replica beta is now a fully functional server instance.

β=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 f
(1 row)

Page 8: Failback

Source slide page 8: Failback{#source-figure-008 caption=“Source slide page 8: Failback”}

Speaker notes

After promotion, the former replica acts as a standalone server. It processes client queries and generates its own WAL archive – within a new timeline.

If the switchover wasn’t caused by a serious server failure, you’ll need to perform a failback: quickly bring that server back online – this time as a replica.

Page 9: Simple Reconnect

Source slide page 9: Simple Reconnect{#source-figure-009 caption=“Source slide page 9: Simple Reconnect”}

The former leader connects to the new leader as a replica

  • works only if the replica received all WAL records from the former
  • leader before the switchover

Speaker notes

When the leader is shut down cleanly (using the fast or the smart shutdown mode), all of its WAL records will most likely reach the replica, though this isn’t guaranteed. The shutdown procedure first terminates all backend processes, then performs a checkpoint, and only at the very end stops wal sender, giving the replica time to receive the checkpoint record.

You can check the leader’s position in WAL with pg_controldata (look for “latest checkpoint location”), while the replica’s position is shown by the pg_last_wal_receive_lsn function. This function returns the current position in WAL, so its value must be ahead of the latest checkpoint location by the size of the checkpoint record.

If all records were actually received, then the former leader can immediately connect to the new one just by adjusting the configuration accordingly.

If the leader is stopped without performing a checkpoint (due to a crash or shutdown in the immediate mode), such a connection will likely be impossible on a server with a current load. The server won’t be able to start, and an error will be logged.

Page 10: Rejoining the Cluster: Direct Connection

Bring the alpha server back online, but this time as a replica. Create the standby.signal file:

postgres$ touch /var/lib/postgresql/16/alpha/standby.signal

Configure the connection to the primary.

postgres$ cat << EOF >> /var/lib/postgresql/16/alpha/postgresql.auto.conf
primary_conninfo = 'user=student port=5433'
EOF

Start alpha and verify that it has become a replica.

student$ sudo pg_ctlcluster 16 alpha start
student$ psql -d replica_switchover
α=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 t
(1 row)

On the beta server, we can modify data:

β=> INSERT INTO test VALUES ('The beta server is now the primary, and alpha is running as a replica.');
INSERT 0 1

The alpha replica receives them.

α=> SELECT * FROM test;
                                   s
------------------------------------------------------------------------
 Initial state. The only server is alpha.
 Replication check: alpha – primary, beta — replica.
 Before graceful stop of primary alpha.
 The beta server is now the primary, and alpha is running as a replica.
(4 rows)

Page 11: Backup

Source slide page 11: Backup{#source-figure-011 caption=“Source slide page 11: Backup”}

The former leader is restored from a backup

  • creates a completely new replica, takes a long time
  • can be expedited with rsync if not much time have passed
  • (still takes a while for large databases)

Speaker notes

When a leader crashes, there’s a good chance that some WAL records don’t make it to the replica. You can’t just reconnect the former leader as a replica in this case.

The simplest and most reliable approach is to build a completely new replica by creating and deploying a fresh base backup. However, for large databases, this can take a long time.

A variation of this approach is to skip pg_basebackup and instead create the backup using the backup API and rsync. If you create the backup immediately after promoting the replica, most files won’t have changed yet, which can significantly speed up the process.

But naturally, this does complicate things.

Page 12: pg_rewind

Source slide page 12: pg_rewind{#source-figure-012 caption=“Source slide page 12: pg_rewind”}

Preparing for Recovery

  • revert the lost WAL records by replacing the affected pages on the target
  • server with pages from the source server
  • copy all system files from the source server

Speaker notes

An even faster approach is to use the stock tool pg_rewind.

It identifies where the two servers diverged, finds the nearest common checkpoint before that point, and by scanning the WAL, determines which pages have been modified since then.

These pages are then replaced with their counterparts from the source server (the new leader), and there shouldn’t be many of them. The utility also copies all system files from the source server.

The server then applies all necessary WAL records from the new leader. At this point, it’s no longer the pg_rewind doing the work, but rather the standard recovery process that runs when the server starts up. To ensure recovery starts from the correct point, the utility creates a backup_label control file.

https://postgrespro.com/docs/postgresql/16/app-pgrewind

Page 13: Considerations with pg_rewind

Target server (former leader)

  • all necessary WAL files must be either in pg_wal or in the archive
  • checksums must be enabled or wal_log_hints must be on
  • the parameter full_page_writes must be on
  • Source server (current leader)
    • clinet-server connection:
    • -source-server = connection string
    • cleanly shut down server:
    • -source-pgdata = directory

Speaker notes

For the source server, if it is running, the –source-server switch specifies the libpq client-server connection string (the utility does not use the replication protocol). The connecting role must have the privileges required to run pg_ls_dir, pg_stat_file, and pg_read_binary_file functions.

If the source server was shut down cleanly, you can just point to its data directory with the –source-pgdata switch.

Several specific requirements must be met for the tool to work:

● All WAL segments containing records from the last common checkpoint to the current moment must be either in the target server’s pg_wal directory or present and available from the WAL archive. In the second case, you need to set the restore_command parameter and the –restore-target-wal switch.

● Due to errors during writing, data mismatch within the pages is possible (the non-atomic writing problem, discussed in the “Base backup” module). This is solved by having the target server write each page’s full image to WAL after it is first changed since the checkpoint. This is enabled by full_page_writes = on. This mode does ignores “minor” page changes (hint bits), so the cluster also must either have page checksum calculation enabled or have wal_log_hints = on.

● If the target server was shut down incorrectly and without a checkpoint, the tool will start it in single-user mode and immediately perform a clean shutdown.

Page 14: Rejoining the Cluster: pg_rewind

Let’s now simulate a split-brain scenario – two writable servers resulting from a network partition (we cover this and other types of failures in detail in the “Cluster Technologies Overview” topic). Promote the alpha server out of replica mode without stopping the primary beta server.

student$ sudo pg_ctlcluster 16 alpha promote

Both alpha and beta can now accept write operations:

β=> INSERT INTO test VALUES ('Die hard');
INSERT 0 1

In practice, this situation must be avoided at all costs, since it’s no longer clear which server to trust. You’ll have to either discard all changes on one of the servers or figure out how to merge the data. We choose to discard the changes made on the beta server. Roll back the conflicting changes on beta – those that diverge from the changes made on alpha. To do this, we’ll use the pg_rewind utility – so let’s first verify that the required parameter values are set on the target server (beta):

β=> \dconfig data_checksums|full_page_writes
List of configuration parameters
    Parameter     | Value
------------------+-------
 data_checksums   | on
 full_page_writes | on
(2 rows)

Stop the target server ungracefully.

student$ sudo kill -QUIT $(sudo head -n 1 /var/lib/postgresql/16/beta/postmaster.pid)

The pg_rewind utility requires you to specify the target server’s PGDATA directory and how to connect to the source server (alpha): either a connection string (if the server is running) or the path to its PGDATA directory (if it is stopped). In our case, the alpha server is running. Since the target server beta was not shut down properly, the utility will start it in single-user mode and stop it after performing a checkpoint. To start it, the configuration file location must be provided. The -R option ensures the target server starts as a replica once the utility finishes: after consistency is restored, it will remain in recovery mode.

postgres$ /usr/lib/postgresql/16/bin/pg_rewind \
  -D /var/lib/postgresql/16/beta \
  --config-file=/etc/postgresql/16/beta/postgresql.conf \
  -R \
  --source-server='user=student port=5432'
pg_rewind: executing "/usr/lib/postgresql/16/bin/postgres" for target server to complete
crash recovery
2026-09-16 23:59:46.566 MSK [78455] LOG:  database system was interrupted; last known up
at 2026-09-16 23:59:38 MSK
2026-09-16 23:59:46.567 MSK [78455] LOG:  database system was not properly shut down;
automatic recovery in progress
2026-09-16 23:59:46.569 MSK [78455] LOG:  redo starts at 0/30002A8
2026-09-16 23:59:46.570 MSK [78455] LOG:  invalid record length at 0/3006AA0: expected at
least 24, got 0
2026-09-16 23:59:46.570 MSK [78455] LOG:  redo done at 0/3006A60 system usage: CPU: user:
0.00 s, system: 0.00 s, elapsed: 0.00 s
2026-09-16 23:59:46.572 MSK [78455] LOG:  checkpoint starting: end-of-recovery immediate
wait
2026-09-16 23:59:46.573 MSK [78455] LOG:  checkpoint complete: wrote 9 buffers (0.1%); 0
WAL file(s) added, 0 removed, 0 recycled; write=0.001 s, sync=0.001 s, total=0.002 s;
sync files=0, longest=0.000 s, average=0.000 s; distance=25 kB, estimate=25 kB;
lsn=0/3006AA0, redo lsn=0/3006AA0
PostgreSQL stand-alone backend 16.14 (Ubuntu 16.14-1.pgdg24.04+1)
backend> 2026-09-16 23:59:46.581 MSK [78455] LOG:  checkpoint starting: shutdown immediate
2026-09-16 23:59:46.581 MSK [78455] LOG:  checkpoint complete: wrote 0 buffers (0.0%); 0
WAL file(s) added, 0 removed, 0 recycled; write=0.001 s, sync=0.001 s, total=0.001 s;
sync files=0, longest=0.000 s, average=0.000 s; distance=0 kB, estimate=23 kB;
lsn=0/3006B18, redo lsn=0/3006B18

Page 15: Page 15

pg_rewind: servers diverged at WAL location 0/3002AE8 on timeline 2
pg_rewind: rewinding from last common checkpoint at 0/30028B0 on timeline 2
pg_rewind: Done!

As a result, pg_rewind “rolled back” the data pages to the last checkpoint before the point where the two servers diverged. The utility also created a backup_label file containing the WAL position from which recovery should begin. Let’s take a look inside:

postgres$ cat /var/lib/postgresql/16/beta/backup_label
START WAL LOCATION: 0/30002A8 (file 000000020000000000000003)
CHECKPOINT LOCATION: 0/30028B0
BACKUP METHOD: pg_rewind
BACKUP FROM: standby
START TIME: 2026-09-16 23:59:47 MSK

By using the -R option, we instructed the utility to create the standby.signal file and set the connection string in the configuration file.

postgres$ ls -l /var/lib/postgresql/16/beta/standby.signal
-rw------- 1 postgres postgres 0 Sep 16 23:59 /var/lib/postgresql/16/beta/standby.signal

A replication slot has already been created on the alpha server – let’s make use of it.

postgres$ echo primary_slot_name = 'replica' >> /var/lib/postgresql/16/beta/postgresql.auto.conf
postgres$ cat /var/lib/postgresql/16/beta/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
checkpoint_timeout = '1d'
primary_conninfo = 'user=student port=5433'
primary_conninfo = 'user=student passfile=''/var/lib/postgresql/.pgpass''
channel_binding=prefer port=5432 sslmode=prefer sslnegotiation=postgres sslcompression=0
sslcertmode=allow sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=prefer
krbsrvname=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable'
primary_slot_name = replica

Now beta will start up as a replica.

student$ sudo pg_ctlcluster 16 beta start

The replication slot has been initialized and is now in use:

α=> SELECT * FROM pg_replication_slots \gx
-[ RECORD 1 ]-------+----------
slot_name           | replica
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | t
active_pid          | 78737
xmin                |
catalog_xmin        |
restart_lsn         | 0/305E000
confirmed_flush_lsn |
wal_status          | reserved
safe_wal_size       |
two_phase           | f
conflicting         |

Data modified on the new primary (alpha) has been received by the replica (beta):

student$ psql -p 5433 -d replica_switchover
β=> SELECT * FROM test;
                                   s
------------------------------------------------------------------------
 Initial state. The only server is alpha.
 Replication check: alpha – primary, beta — replica.
 Before graceful stop of primary alpha.
 The beta server is now the primary, and alpha is running as a replica.
(4 rows)

Let’s add one more row:

α=> INSERT INTO test VALUES ('Another row from alpha.');

Page 16: Page 16

INSERT 0 1
β=> SELECT * FROM test;
                                   s
------------------------------------------------------------------------
 Initial state. The only server is alpha.
 Replication check: alpha – primary, beta — replica.
 Before graceful stop of primary alpha.
 The beta server is now the primary, and alpha is running as a replica.
 Another row from alpha.
(5 rows)

Now the former primary (beta) is back into service as a replica.

Page 17: Switchover and Archiving

Source slide page 17: Switchover and Archiving{#source-figure-017 caption=“Source slide page 17: Switchover and Archiving”}

Speaker notes

The write-ahead log archive, populated through continuous archiving, has an problem when it comes to streaming replication and switchover:

on a leader failure, some WAL segments may not make it into the archive. For example, if the disk runs out of space, archive_command willl return an error.

Page 18: Switchover and Archiving

Source slide page 18: Switchover and Archiving{#source-figure-018 caption=“Source slide page 18: Switchover and Archiving”}

Speaker notes

The replica is unaware of the archiving settings on the leader. When the former replica takes over as the leader, it won’t write the missing file segments to the archive (even though it could) because it assumes the archive was functioning properly all along. The archive will end up incomplete.

This means you can only restore the system from existing backups up to the point where this gap occurred. If this situation has occurred (and you detect it in time), you must take a backup immediately.

Stream archiving doesn’t have this problem because the pg_receivewal utility monitors the directory contents and requests any missing WAL records from the server. However, as mentioned in the “Backup” module, using this tool means maintaining additional infrastructure.

Page 19: File Archive Issues

Let’s restore the alpha and beta servers from a backup and configure file archiving to a shared archive on both.

student$ sudo pg_ctlcluster 16 alpha stop
student$ sudo pg_ctlcluster 16 beta stop
postgres$ rm -rf /var/lib/postgresql/16/alpha
postgres$ mkdir /var/lib/postgresql/16/alpha
postgres$ chmod 700 /var/lib/postgresql/16/alpha
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ mkdir /var/lib/postgresql/16/beta
postgres$ chmod 700 /var/lib/postgresql/16/beta
student$ sudo tar xzf /home/student/tmp/backup/base.tar.gz -C /var/lib/postgresql/16/alpha
student$ sudo tar xzf /home/student/tmp/backup/pg_wal.tar.gz -C /var/lib/postgresql/16/alpha/pg_wal
student$ sudo tar xzf /home/student/tmp/backup/base.tar.gz -C /var/lib/postgresql/16/beta
student$ sudo tar xzf /home/student/tmp/backup/pg_wal.tar.gz -C /var/lib/postgresql/16/beta/pg_wal

WAL segment archive.

postgres$ rm -rf /var/lib/postgresql/archive
postgres$ mkdir /var/lib/postgresql/archive

Identical archiving settings on both the alpha and beta servers.

postgres$ cat << EOF | tee -a \
  /var/lib/postgresql/16/alpha/postgresql.auto.conf \
  /var/lib/postgresql/16/beta/postgresql.auto.conf
archive_mode = on
archive_command = 'test ! -f /var/lib/postgresql/archive/%f && cp %p /var/lib/postgresql/archive/%f'
EOF
archive_mode = on
archive_command = 'test ! -f /var/lib/postgresql/archive/%f && cp %p
/var/lib/postgresql/archive/%f'

On the replica (beta), let’s create the standby.signal file and configure the connection to the primary.

postgres$ touch /var/lib/postgresql/16/beta/standby.signal
postgres$ cat << EOF >> /var/lib/postgresql/16/beta/postgresql.auto.conf
primary_conninfo = 'user=student port=5432'
primary_slot_name = 'replica'
EOF

Start the primary (alpha) and create a replication slot.

student$ sudo pg_ctlcluster 16 alpha start
student$ pg_receivewal --create-slot --slot=replica

Start the replica.

student$ sudo pg_ctlcluster 16 beta start

Current WAL segment:

student$ psql -d replica_switchover
α=> SELECT pg_walfile_name(pg_current_wal_lsn());
     pg_walfile_name
--------------------------
 000000010000000000000003
(1 row)

Force a WAL segment switch. For the switch to occur, we need to ensure that both the current and next segments contain some records.

α=> SELECT pg_switch_wal();

Page 20: Page 20

 pg_switch_wal
---------------
 0/3006658
(1 row)
α=> INSERT INTO test SELECT now();
INSERT 0 1

Now log entries are being written to the next segment, while the previous one has been archived:

α=> SELECT
  pg_walfile_name(pg_current_wal_lsn()) current_wal,
  last_archived_wal,
  last_failed_wal
FROM pg_stat_archiver;
       current_wal        |    last_archived_wal     | last_failed_wal
--------------------------+--------------------------+-----------------
 000000010000000000000004 | 000000010000000000000003 |
(1 row)

Let’s say there is an issue with archiving. The cause could be something like a running out of space on disk, or network connectivity issues – we’ll simulate this by returning status 1 from the archive command.

α=> ALTER SYSTEM SET archive_command = 'exit 1';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha reload

Let’s force another WAL segment switch.

α=> SELECT pg_switch_wal();
 pg_switch_wal
---------------
 0/4002BE0
(1 row)
α=> INSERT INTO test SELECT now();
INSERT 0 1

The segment is not being archived.

α=> SELECT
  pg_walfile_name(pg_current_wal_lsn()) current_wal,
  last_archived_wal,
  last_failed_wal
FROM pg_stat_archiver;
       current_wal        |    last_archived_wal     |     last_failed_wal
--------------------------+--------------------------+--------------------------
 000000010000000000000005 | 000000010000000000000003 | 000000010000000000000004
(1 row)

The archiver process will keep retrying, but without success.

student$ tail -n 4 /var/log/postgresql/postgresql-16-alpha.log
2026-09-17 00:01:06.202 MSK [79509] LOG:  archive command failed with exit code 1
2026-09-17 00:01:06.202 MSK [79509] DETAIL:  The failed archive command was: exit 1
2026-09-17 00:01:07.204 MSK [79509] LOG:  archive command failed with exit code 1
2026-09-17 00:01:07.204 MSK [79509] DETAIL:  The failed archive command was: exit 1

The beta server wasn’t archiving while in replica mode, and after promotion it won’t archive the missed segment either. Stop alpha and promote beta.

student$ sudo kill -QUIT $(sudo head -n 1 /var/lib/postgresql/16/alpha/postmaster.pid)
student$ sudo pg_ctlcluster 16 beta promote

Force another WAL segment switch, this time on beta.

student$ psql -p 5433 -d replica_switchover
β=> SELECT pg_switch_wal();

Page 21: Page 21

 pg_switch_wal
---------------
 0/5003E50
(1 row)
β=> INSERT INTO test SELECT now();
INSERT 0 1

How’s the archive doing?

student$ ls -l /var/lib/postgresql/archive
total 49156
-rw------- 1 postgres postgres 16777216 Sep 17 00:01 000000010000000000000003
-rw------- 1 postgres postgres 16777216 Sep 17 00:01 000000010000000000000005.partial
-rw------- 1 postgres postgres 16777216 Sep 17 00:01 000000020000000000000005
-rw------- 1 postgres postgres       41 Sep 17 00:01 00000002.history

Segment 000000010000000000000004 is missing – the archive is now unusable for recovery and replication.

Page 22: Switchover and Archiving

Source slide page 22: Switchover and Archiving{#source-figure-022 caption=“Source slide page 22: Switchover and Archiving”}

Speaker notes

With archive_mode = always, the replica starts an archiver process that writes segments to the archive just like the leader server does. This means that the same file will be recorded twice, by two servers. This places two strict requirements on archive_command:

● it must not overwrite an existing file, but should report success if a file with identical content is already present in the archive;

● it must handle concurrent write attempts from both servers correctly.

With these requirements met, the segment won’t be lost because the replica will keep trying to write it even after the leader server stops.

Of course, you could configure archive_command so that the leader and the replica save WAL segments to separate archives, but that’s hardly practical.

Page 23: Archiving from a Replica

To keep the archive intact when failing over to a replica, the replica must have archive_mode = always. The archive command must correctly handle the case where both the primary and the replica write the same segment simultaneously. Let’s restore both alpha and beta from backup again and configure file-based archiving to a shared archive on both servers.

student$ sudo pg_ctlcluster 16 alpha stop
Cluster is not running.
student$ sudo pg_ctlcluster 16 beta stop
postgres$ rm -rf /var/lib/postgresql/16/alpha
postgres$ mkdir /var/lib/postgresql/16/alpha
postgres$ chmod 700 /var/lib/postgresql/16/alpha
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ mkdir /var/lib/postgresql/16/beta
postgres$ chmod 700 /var/lib/postgresql/16/beta
student$ sudo tar xzf /home/student/tmp/backup/base.tar.gz -C /var/lib/postgresql/16/alpha
student$ sudo tar xzf /home/student/tmp/backup/pg_wal.tar.gz -C /var/lib/postgresql/16/alpha/pg_wal
student$ sudo tar xzf /home/student/tmp/backup/base.tar.gz -C /var/lib/postgresql/16/beta
student$ sudo tar xzf /home/student/tmp/backup/pg_wal.tar.gz -C /var/lib/postgresql/16/beta/pg_wal

WAL segment archive.

postgres$ rm -rf /var/lib/postgresql/archive
postgres$ mkdir /var/lib/postgresql/archive

Configuration of archiving on the primary (alpha). We replaced the logical operator in the archive command so that it immediately returns 0 if the file already exists.

postgres$ cat << EOF | tee -a /var/lib/postgresql/16/alpha/postgresql.auto.conf
archive_mode = on
archive_command = 'test -f /var/lib/postgresql/archive/%f || cp %p /var/lib/postgresql/archive/%f'
EOF
archive_mode = on
archive_command = 'test -f /var/lib/postgresql/archive/%f || cp %p
/var/lib/postgresql/archive/%f'

Configuration of archiving on the replica (beta). Use the always mode and the same archive command.

postgres$ cat << EOF | tee -a /var/lib/postgresql/16/beta/postgresql.auto.conf
archive_mode = always
archive_command = 'test -f /var/lib/postgresql/archive/%f || cp %p /var/lib/postgresql/archive/%f'
primary_conninfo = 'user=student port=5432'
primary_slot_name = 'replica'
EOF
archive_mode = always
archive_command = 'test -f /var/lib/postgresql/archive/%f || cp %p
/var/lib/postgresql/archive/%f'
primary_conninfo = 'user=student port=5432'
primary_slot_name = 'replica'

The replica also needs a standby.signal file.

postgres$ touch /var/lib/postgresql/16/beta/standby.signal

Start the primary and create a replication slot.

student$ sudo pg_ctlcluster 16 alpha start
student$ pg_receivewal --create-slot --slot=replica

Start the replica.

student$ sudo pg_ctlcluster 16 beta start
student$ psql -d replica_switchover

Page 24: Page 24

Repeat the experiment. Switch the segment:

α=> SELECT pg_switch_wal();
 pg_switch_wal
---------------
 0/3006620
(1 row)
α=> INSERT INTO test SELECT now();
INSERT 0 1

Checking the archiving status:

α=> SELECT
  pg_walfile_name(pg_current_wal_lsn()) current_wal,
  last_archived_wal,
  last_failed_wal
FROM pg_stat_archiver;
       current_wal        |    last_archived_wal     | last_failed_wal
--------------------------+--------------------------+-----------------
 000000010000000000000004 | 000000010000000000000003 |
(1 row)

The completed segment has been archived. The alpha server is experiencing archiving issues – the command returns 1:

α=> ALTER SYSTEM SET archive_command = 'exit 1';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha reload

Alpha continues to generate WAL segments.

α=> SELECT pg_switch_wal();
 pg_switch_wal
---------------
 0/4002BE0
(1 row)
α=> INSERT INTO test SELECT now();
INSERT 0 1

But the primary does not archive them:

α=> SELECT
  pg_walfile_name(pg_current_wal_lsn()) current_wal,
  last_archived_wal,
  last_failed_wal
FROM pg_stat_archiver;
       current_wal        |    last_archived_wal     |     last_failed_wal
--------------------------+--------------------------+--------------------------
 000000010000000000000005 | 000000010000000000000003 | 000000010000000000000004
(1 row)

However, archiving from the replica succeeds and the segment ends up in the archive:

student$ ls -l /var/lib/postgresql/archive
total 32768
-rw------- 1 postgres postgres 16777216 Sep 17 00:01 000000010000000000000003
-rw------- 1 postgres postgres 16777216 Sep 17 00:01 000000010000000000000004

Promote the replica.

student$ sudo kill -QUIT $(sudo head -n 1 /var/lib/postgresql/16/alpha/postmaster.pid)
student$ sudo pg_ctlcluster 16 beta promote

Beta is now the primary server and is generating WAL files.

student$ psql -p 5433 -d replica_switchover
β=> SELECT pg_switch_wal();

Page 25: Page 25

 pg_switch_wal
---------------
 0/5002190
(1 row)
β=> INSERT INTO test SELECT now();
INSERT 0 1

Let’s check the archive again:

student$ ls -l /var/lib/postgresql/archive
total 65540
-rw------- 1 postgres postgres 16777216 Sep 17 00:01 000000010000000000000003
-rw------- 1 postgres postgres 16777216 Sep 17 00:01 000000010000000000000004
-rw------- 1 postgres postgres 16777216 Sep 17 00:01 000000010000000000000005.partial
-rw------- 1 postgres postgres 16777216 Sep 17 00:01 000000020000000000000005
-rw------- 1 postgres postgres       41 Sep 17 00:01 00000002.history

The replica’s files have appeared in the archive, there are no gaps, and the problem is resolved.

Page 26: Takeaways

Switchover is used both during normal operation and in emergency situations

  • After a switchover, the former leader must be brought back online
  • Both procedures must be tried and tested in advance
  • The write-ahead log archive requires extra attention

Page 27: Practice

1. Set up streaming replication from alpha to beta, with a replication slot, without continuous archiving.

    1. Simulate alpha server failure and switch over to beta. 3. Bring alpha back online as a replica by backing up from the new leader beta and adjusting the configuration.
  • Verify that replication is functioning and using the slot. 4. Switch back to alpha so that the former leader becomes the main server again.

Speaker notes

To simulate a failure, you can stop the server in immediate mode:

pg_ctlcluster 16 cluster_name stop -m immediate –skip-systemctl-redirect

or terminate the main postgres process:

kill -QUIT номер_процесса

The process ID is the first line of the PGDATA/postmaster.pid file.

After completing the practice tasks, delete the slot and the created database, and reset the configuration.

Page 28: 1. Setting Up Replication Without an Archive

Set up replication. Stop the beta server and clear its data directory.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta

Create a standalone backup in the beta server’s data directory, first creating the replica slot.

postgres$ pg_basebackup \
  --checkpoint=fast \
  --pgdata=/var/lib/postgresql/16/beta \
  -R \
  --create-slot \
  --slot=replica
postgres$ cat /var/lib/postgresql/16/beta/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
primary_conninfo = 'user=postgres passfile=''/var/lib/postgresql/.pgpass''
channel_binding=prefer host=''/var/run/postgresql'' port=5432 sslmode=prefer
sslnegotiation=postgres sslcompression=0 sslcertmode=allow sslsni=1
ssl_min_protocol_version=TLSv1.2 gssencmode=prefer krbsrvname=postgres gssdelegation=0
target_session_attrs=any load_balance_hosts=disable'
primary_slot_name = 'replica'

Start the server in replica mode.

student$ sudo pg_ctlcluster 16 beta start

Check the configuration. Run several commands on the primary server:

α=> CREATE DATABASE replica_switchover;
CREATE DATABASE
α=> \c replica_switchover
You are now connected to database "replica_switchover" as user "student".
α=> CREATE TABLE test(s text);
CREATE TABLE
α=> INSERT INTO test VALUES ('Hello, world!');
INSERT 0 1

Check the replica:

student$ psql -p 5433
β=> \c replica_switchover
You are now connected to database "replica_switchover" as user "student".
β=> SELECT * FROM test;
       s
---------------
 Hello, world!
(1 row)

2. Primary Server Failure and Failover to Replica

Simulate a primary server crash.

student$ sudo kill -QUIT $(sudo head -n 1 /var/lib/postgresql/16/alpha/postmaster.pid)

Promote the replica.

student$ sudo pg_ctlcluster 16 beta promote

Page 29: 3. Failback on Former Primary

Create a standalone backup of the beta server in the alpha server’s data directory. Before doing that, delete the configuration file – otherwise pg_basebackup will copy it and append parameters to it.

postgres$ rm -f /var/lib/postgresql/16/beta/postgresql.auto.conf

Clear the data directory on alpha.

postgres$ rm -rf /var/lib/postgresql/16/alpha

Run the backup utility to create a replication slot and replication configuration.

postgres$ pg_basebackup \
  --port=5433 \
  --checkpoint=fast \
  --pgdata=/var/lib/postgresql/16/alpha \
  -R \
  --create-slot \
  --slot=replica

Configuration parameters and the standby signal file have been set up:

postgres$ cat /var/lib/postgresql/16/alpha/postgresql.auto.conf
primary_conninfo = 'user=postgres passfile=''/var/lib/postgresql/.pgpass''
channel_binding=prefer port=5433 sslmode=prefer sslnegotiation=postgres sslcompression=0
sslcertmode=allow sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=prefer
krbsrvname=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable'
primary_slot_name = 'replica'
postgres$ ls -l /var/lib/postgresql/16/alpha/standby.signal
-rw------- 1 postgres postgres 0 Sep 17 00:13 /var/lib/postgresql/16/alpha/standby.signal
student$ sudo pg_ctlcluster 16 alpha start

The replication slot has been initialized and is now in use:

β=> SELECT * FROM pg_replication_slots \gx
-[ RECORD 1 ]-------+----------
slot_name           | replica
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | t
active_pid          | 105330
xmin                |
catalog_xmin        |
restart_lsn         | 0/5000060
confirmed_flush_lsn |
wal_status          | reserved
safe_wal_size       |
two_phase           | f
conflicting         |

Check the replication:

β=> INSERT INTO test VALUES ('I am the former replica (now primary).');
INSERT 0 1
student$ psql -d replica_switchover
α=> SELECT * FROM test;
                   s
----------------------------------------
 Hello, world!
 I am the former replica (now primary).
(2 rows)

4. Switching to a New Replica

Promote the replica.

Page 30: Page 30

student$ sudo pg_ctlcluster 16 alpha promote
α=> select pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 f
(1 row)

The former primary is now primary again.

Cleanup

Delete the replication slot and database, and reset configuration.

α=> \c postgres
You are now connected to database "postgres" as user "student".
α=> DROP DATABASE replica_switchover;
DROP DATABASE
student$ sudo pg_ctlcluster 16 alpha restart
β=> \c postgres
You are now connected to database "postgres" as user "student".
β=> SELECT pg_drop_replication_slot('replica');
 pg_drop_replication_slot
--------------------------
(1 row)
student$ sudo pg_ctlcluster 16 beta stop

Page 31: Practice+

  1. Configure the alpha server to retain two additional WAL segments after each checkpoint. Start physical replication from alpha to beta without a replication slot. 2. Crash alpha and promote beta. Restart alpha and modify some data on it. Verify that the data on alpha and beta do not match. 3. Stop alpha and start it as a replica of beta using pg_rewind.

Speaker notes

  1. Set up replication with the simplest settings and without a replication slot. To ensure that two additional WAL segments are retained, set a suitable wal_keep_size value.

After completing the practice tasks, delete the slot and the created database, and reset the configuration.

Page 32: 1. Replication

Since we are not using replication slots, we will retain two additional WAL file segments.

α=> ALTER SYSTEM SET wal_keep_size = 32;
ALTER SYSTEM

Stop the beta server and make it a replica of the alpha server.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ pg_basebackup --checkpoint=fast -R --pgdata=/var/lib/postgresql/16/beta
student$ sudo pg_ctlcluster 16 beta start

Create a database and a table in it.

α=> CREATE DATABASE replica_switchover;
CREATE DATABASE
α=> \c replica_switchover
You are now connected to database "replica_switchover" as user "student".
α=> CREATE TABLE test AS SELECT 'alpha is primary.' txt;
SELECT 1

Changes have been replicated to beta.

student$ psql -p 5433 -d replica_switchover
β=> SELECT * FROM test;
        txt
-------------------
 alpha is primary.
(1 row)

Check the replication status.

α=> SELECT * FROM pg_stat_replication \gx
-[ RECORD 1 ]----+------------------------------
pid              | 106207
usesysid         | 10
usename          | postgres
application_name | 16/beta
client_addr      |
client_hostname  |
client_port      | -1
backend_start    | 2026-09-17 00:14:57.461258+03
backend_xmin     |
state            | streaming
sent_lsn         | 0/3432298
write_lsn        | 0/3432298
flush_lsn        | 0/3432298
replay_lsn       | 0/3432298
write_lag        | 00:00:00.000277
flush_lag        | 00:00:00.008669
replay_lag       | 00:00:00.008694
sync_priority    | 0
sync_state       | async
reply_time       | 2026-09-17 00:15:04.970441+03

On the replica, you can view the WAL receiver statistics:

β=> SELECT pid, status, written_lsn FROM pg_stat_wal_receiver;

Page 33: 2. Replication Break

  pid   |  status   | written_lsn
--------+-----------+-------------
 106206 | streaming | 0/3432298
(1 row)

The primary server has lost power.

student$ sudo kill -QUIT $(sudo head -n 1 /var/lib/postgresql/16/alpha/postmaster.pid)

Promote beta.

student$ sudo pg_ctlcluster 16 beta promote

Make changes on the new primary.

β=> INSERT INTO test VALUES('beta promoted, handles write transactions.');
INSERT 0 1

Power has been restored to the alpha server.

student$ sudo pg_ctlcluster 16 alpha start
student$ psql -d replica_switchover
α=> INSERT INTO test VALUES('Die Hard...');
INSERT 0 1

The data on the two servers has diverged.

α=> SELECT * FROM test;
        txt
-------------------
 alpha is primary.
 Die Hard...
(2 rows)
β=> SELECT * FROM test;
                    txt
--------------------------------------------
 alpha is primary.
 beta promoted, handles write transactions.
(2 rows)

Stop alpha (former primary).

student$ sudo pg_ctlcluster 16 alpha stop

3. Failback on Former Primary

We want to bring the alpha server back up as a replica. To do this, we need to bring it back to a state compatible with beta. We can use the stock utility for that.

postgres$ /usr/lib/postgresql/16/bin/pg_rewind \
  -D /var/lib/postgresql/16/alpha \
  --config-file=/etc/postgresql/16/alpha/postgresql.conf \
  --source-server='user=postgres port=5433' \
  -R
pg_rewind: servers diverged at WAL location 0/3432298 on timeline 1
pg_rewind: rewinding from last common checkpoint at 0/2000060 on timeline 1
pg_rewind: Done!

Write transactions are running on the beta server.

β=> INSERT INTO test VALUES('Changes still appear on beta.');
INSERT 0 1

Data changed on the beta server has been received by the alpha server:

student$ sudo pg_ctlcluster 16 alpha start
student$ psql -d replica_switchover

Page 34: Cleanup

Source slide page 34: Cleanup{#source-figure-034 caption=“Source slide page 34: Cleanup”}

α=> SELECT * FROM test;
                    txt
--------------------------------------------
 alpha is primary.
 beta promoted, handles write transactions.
 Changes still appear on beta.
(3 rows)

Delete the database we created and reset configuration.

β=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
β=> DROP DATABASE replica_switchover;
DROP DATABASE
β=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta stop
α=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
α=> SELECT pg_promote();
 pg_promote
------------
 t
(1 row)
α=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

06 Lesson 06: Replication / Logical Replication

Native Markdown source page: Replication / Logical Replication
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: No matching course-page link was captured in the source inventory. Archive: DBA3-handouts-16-en-20260917.zip PDF pages: 42

Page 1: Replication / Logical Replication

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Alexey Beresnev Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Logical vs Physical Replication
  • Publications and Subscriptions
  • Initial Synchronization
  • Row Identification and Filtration
  • Logical Decoding
  • Large Transactions
  • Conflicts
  • Replication Sources

Page 3: Comparison

Physical

  • leader-replica: data flow in one direction only
  • stream WAL records or send WAL files
  • binary server compatibility is required
  • whole cluster replication only
  • Logical
    • publication-subscriptions: servers have no dedicated roles
    • row-level changes are replicated
    • protocol-level compatibility is required
    • can replicate individual tables

Speaker notes

During physical replication, one server is assigned the leader role and others become replicas. The leader transfers WAL records to replicas to be applied to data files there. The replica does not “understand” the changes, just applies them mechanically without any interpretation of what the changes mean, so binary compatibility between servers is necessary (the same platforms and major PostgreSQL versions). Since the WAL is shared across the entire cluster, only the cluster as a whole can be replicated.

In logical replication, one server creates a publication that other servers can subscribe to. Servers don’t have fixed roles: the same server can both publish changes and subscribe to publications from other servers (or even its own).

By default, information about table row changes is transmitted to the subscription in a text format that’s platform-independent and doesn’t require binary compatibility.

https://postgrespro.com/docs/postgresql/16/logical-replication

Page 4: How It Works

Source slide page 4: How It Works{#source-figure-004 caption=“Source slide page 4: How It Works”}

  • max_wal_senders = 10
  • max_logical_replication_workers = 4
  • max_replication_slots = 10
  • max_worker_processes = 8

Speaker notes

Logical replication uses the publish-subscribe pattern.

When the instance starts, the logical replication launcher background process starts with it, periodically checking the pg_subscription table in the system catalog for new subscriptions. As soon as a subscription appears, a background logical replication apply worker process is launched to handle it. It connects to the publishing server, which, like any replication connection, starts the wal sender process. This process reads the WAL, selects the necessary records and converts them to platform-independent protocol messages. A logical replication slot is always used for that.

On the subscription side, the worker receives messages and applies changes. Meanwhile, the subscriber server can receive normal read and write queries.

Note that the publishing server can run multiple wal sender processes, one for each subscription. Both max_wal_senders and max_replication_slots must be no less than the number of subscriptions.

On the subscriber server, you need to configure max_logical_replication_workers (for processes that receive subscription changes) and max_worker_processes in general (more by one at least, since there’s also the logical replication launcher process, though this pool is actually used for other purposes as well).

https://postgrespro.com/docs/postgresql/16/logical-replication-architecture

Page 5: Publication

Database object

  • sends data changes row by row in the order they are committed in
  • only for regular and partitioned tables
  • wal_level = logical
  • Published operations
    • insert, update, delete, truncate
  • Privileges
    • creating a new publication – CREATE privilege for the database
    • add table – owner or superuser
    • add all tables for a schema or a database – superuser

Speaker notes

A publication is created on the server to collect changes from one or more tables (regular or partitioned) in the same database. To create a publication, you need the CREATE privilege on the database. To replicate from multiple databases, multiple publications must be created.

To include a table in a publication, you must be either a superuser or the owner of the table. To publish all tables belonging to a schema or a database, you must be a superuser.

For logical replication to work, the publishing server’s WAL must contain additional information (wal_level = logical).

Publishing includes row changes that occur in tables as a result of DML commands. The MERGE command executed on the publishing server is applied as INSERT, UPDATE, or DELETE.

By default, changes are not transferred immediately, but only when the transaction is committed.

https://postgrespro.com/docs/postgresql/16/logical-replication-publication

https://postgrespro.com/docs/postgresql/16/sql-createpublication

Page 6: Subscription

Database object

  • receives messages from one or more publications
  • binary subscription possible
  • applies changes row by row
  • Tables and columns are matched by full names
  • Privileges
    • create subscription – pg_create_subscription role or superuser

Speaker notes

By defining subscriptions to publications in a database, you can receive and apply changes. Basically, a subscription is a connection to the publishing server that serves for data replication.

Members of the pg_create_subscription role or superusers can create subscriptions.

Changes are always applied row by row. Although each individual change doesn’t require query parsing and planning, bulk changes will still apply more slowly because of this row-by-row approach.

Tables are identified by their full names (including the schema), and columns are identified by their names as well. This allows the subscription to use a different data schema (for example, having additional columns in the table).

When creating a subscription, you can specify the parameter binary to transfer the data in binary format. The binary format can speed up processing if binary receive and send functions are defined for the required data types. However, when using binary format, it’s important that the machine representation of data is compatible across different platforms.

https://postgrespro.com/docs/postgresql/16/logical-replication-subscription

Page 7: Publications and Subscriptions

Source slide page 7: Publications and Subscriptions{#source-figure-007 caption=“Source slide page 7: Publications and Subscriptions”}

Speaker notes

You can distribute objects to publications in any way on the publishing server. You can include all tables in one publication, you can split them between multiple publications – this does not affect performance.

However, each subscription creates a separate connection to the publishing server, creating unnecessary load. Several wal sender processes will run on the publishing server, each of which will independently read the WAL and perform logical decoding (more on that below) in its local memory. Therefore, if you need to subscribe to several publications, it is better to do it in one common subscription.

Remember that publications and subscriptions are database objects, so each database will require a separate connection.

Logical replication monitoring on the publishing server is identical to physical replication monitoring. Your most important tools here are the views pg_stat_replication and pg_replication_slots.

Subscription status information is stored in pg_subscription.

https://postgrespro.com/docs/postgresql/16/catalog-pg-subscription#CATAL OG-PG-SUBSCRIPTION

Information about applying changes is found in the pg_stat_subscription view.

https://postgrespro.com/docs/postgresql/16/monitoring-stats#MONITORING-PG-STAT-SUBSCRIPTION

Replication errors go to pg_stat_subscription_stats.

https://postgrespro.ru/docs/postgresql/16/monitoring-stats#MONITORING-P G-STAT-SUBSCRIPTION-STATS

Page 8: Logical Replication

We’re going to set up logical replication for the test table from the alpha server to the beta server. Let’s start by creating a database.

α=> CREATE DATABASE replica_logical;
CREATE DATABASE
α=> \c replica_logical
You are now connected to database "replica_logical" as user "student".

Logical replication requires another WAL level.

α=> ALTER SYSTEM SET wal_level = logical;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

We’ll make the second database cluster a copy of the first one, so let’s back it up to the second server’s data directory.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ pg_basebackup --checkpoint=fast --pgdata=/var/lib/postgresql/16/beta
student$ sudo pg_ctlcluster 16 beta start
student$ psql -d replica_logical

Now let’s create a table on the first server and populate it with data.

α=> CREATE TABLE test(
  id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
  descr text,
  cluster_name text DEFAULT 'alpha'
);
CREATE TABLE

The test table is not present on the second server. Since DDL commands are not replicated, we need to create this table manually. Its structure may be different, as long as all the published tables are present.

student$ psql -p 5433 -d replica_logical
β=> CREATE TABLE test(
  id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
  descr text,
  cluster_name text DEFAULT 'beta',
  additional text
);
CREATE TABLE

Create a publication on the first server.

α=> CREATE PUBLICATION test_pub;
CREATE PUBLICATION
α=> \dRp+
                           Publication test_pub
  Owner  | All tables | Inserts | Updates | Deletes | Truncates | Via root
---------+------------+---------+---------+---------+-----------+----------
 student | f          | t       | t       | t       | t         | f
(1 row)

Notice that the publication was created even though it doesn’t contain any tables yet. Typically, you create a publication by specifying: a list of tables, and in this case you can specify which columns to publish all tables in specified schemas (requires superuser privileges)

Page 9: Page 9

all tables in the database (requires superuser privileges) Let’s add the table to the publication.

α=> ALTER PUBLICATION test_pub ADD TABLE test;
ALTER PUBLICATION
α=> \dRp+
                           Publication test_pub
  Owner  | All tables | Inserts | Updates | Deletes | Truncates | Via root
---------+------------+---------+---------+---------+-----------+----------
 student | f          | t       | t       | t       | t         | f
Tables:
    "public.test"

On the second server, subscribe to the publication:

β=> CREATE SUBSCRIPTION test_sub
CONNECTION 'host=localhost port=5432 user=student password=student dbname=replica_logical'
PUBLICATION test_pub;
NOTICE:  created replication slot "test_sub" on publisher
CREATE SUBSCRIPTION
β=> \dRs
           List of subscriptions
   Name   |  Owner  | Enabled | Publication
----------+---------+---------+-------------
 test_sub | student | t       | {test_pub}
(1 row)

Check the subscription we created.

α=> INSERT INTO test(descr) VALUES ('One');
INSERT 0 1
β=> SELECT * FROM test;
 id | descr | cluster_name | additional
----+-------+--------------+------------
  1 | One   | alpha        |
(1 row)

The values for id, descr and cluster_name columns are retrieved from the publication. Starting with PostgreSQL 15, you can publish only specific columns instead of all of them. Let’s modify the publication:

α=> ALTER PUBLICATION test_pub SET TABLE test (id, descr);
ALTER PUBLICATION
α=> \dRp+
                           Publication test_pub
  Owner  | All tables | Inserts | Updates | Deletes | Truncates | Via root
---------+------------+---------+---------+---------+-----------+----------
 student | f          | t       | t       | t       | t         | f
Tables:
    "public.test" (id, descr)

Check:

α=> INSERT INTO test(descr) VALUES ('Two');
INSERT 0 1
β=> SELECT * FROM test;
 id | descr | cluster_name | additional
----+-------+--------------+------------
  1 | One   | alpha        |
  2 | Two   | beta         |
(2 rows)

Notice that the cname column now has the default value (beta) on the subscriber.

Page 10: Page 10

The following view shows the state of the subscription:

β=> SELECT * FROM pg_stat_subscription \gx
-[ RECORD 1 ]---------+------------------------------
subid                 | 16419
subname               | test_sub
pid                   | 82645
leader_pid            |
relid                 |
received_lsn          | 0/3084B30
last_msg_send_time    | 2026-09-17 00:02:54.298711+03
last_msg_receipt_time | 2026-09-17 00:02:54.299514+03
latest_end_lsn        | 0/3084B30
latest_end_time       | 2026-09-17 00:02:54.298711+03

received_lsn – the LSN up to which changes have been received latest_end_lsn – the LSN confirmed to the WAL sender process The logical replication worker process starts (you can see its ID in pg_stat_subscription.pid):

student$ ps f -C postgres -o pid,command | grep beta
  82278 /usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/beta -c
config_file=/etc/postgresql/16/beta/postgresql.conf
  82279  \_ postgres: 16/beta: checkpointer
  82280  \_ postgres: 16/beta: background writer
  82283  \_ postgres: 16/beta: walwriter
  82284  \_ postgres: 16/beta: autovacuum launcher
  82285  \_ postgres: 16/beta: logical replication launcher
  82410  \_ postgres: 16/beta: student replica_logical [local] idle
  82645  \_ postgres: 16/beta: logical replication apply worker for subscription 16419

Page 11: Limitations

Not replicated

  • sequences materialized views temporary and unlogged tables foreign tables large objects DDL TRUNCATE restriction
    • tables sharing foreign keys must belong to the same subscription

Speaker notes

Only changes to the content of regular and partitioned tables caused by DML commands are replicated.

The contents of the rest of the objects, broadly defined as “relations”, are not replicated. This includes sequences, materialized views, temporary and unlogged tables, foreign tables. Large objects are not replicated.

DDL commands are also not replicated, so you must first create the necessary tables on the subscription side.

TRUNCATE replicates successfully only if all subscriber tables whose foreign keys reference the tables being truncated are included in the same subscription.

https://postgrespro.com/docs/postgresql/16/logical-replication-restrictions

Page 12: Initial Synchronization

Source slide page 12: Initial Synchronization{#source-figure-012 caption=“Source slide page 12: Initial Synchronization”}

One synchronization process per table

  • but no more than max_sync_workers_per_subscription = 2

Speaker notes

By default, if you do not specify a value for copy_data = false, initial table content synchronization occurs when a subscription is created. This happens seamlessly through the use of the snapshot export mechanism.

The initial synchronization should be completed as soon as possible so as not to delay the vacuuming horizon longer than necessary. Therefore, synchronization is usually done in parallel: the apply worker process looks at the list of replicated tables and starts additional workers that do the actual synchronization. One table is always synchronized by one process.

The maximum number of concurrent synchronization processes is limited by max_sync_workers_per_subscription (default is two).

The replication status for each table in the subscription is shown in the pg_subscription_rel view.

https://postgrespro.com/docs/postgresql/16/catalog-pg-subscription-rel#CAT ALOG-PG-SUBSCRIPTION-REL

If the subscription is flagged as binary, then not only replication will be performed in binary format, but also initial synchronization.

Page 13: Initial Synchronization

By default, the subscriber collects table contents first, and replication begins after that. This is seamless, and data retention is guaranteed. Create three tables with a huge number of rows, and a publication:

α=> CREATE TABLE t1 AS
  SELECT id, 'This is row nubmer'||id descr FROM generate_series(1,1_000_000) id;
SELECT 1000000
α=> CREATE TABLE t2 AS
  SELECT id, 'This is row nubmer'||id descr FROM generate_series(1,1_000_000) id;
SELECT 1000000
α=> CREATE TABLE t3 AS
  SELECT id, 'This is row nubmer'||id descr FROM generate_series(1,1_000_000) id;
SELECT 1000000
α=> CREATE PUBLICATION t123_pub FOR TABLE t1, TABLE t2, TABLE t3;
CREATE PUBLICATION

On the subscriber, create empty tables with identical structure:

β=> CREATE TABLE t1( id integer, descr text );
CREATE TABLE
β=> CREATE TABLE t2( id integer, descr text );
CREATE TABLE
β=> CREATE TABLE t3( id integer, descr text );
CREATE TABLE

Subscribe to the publication, then peek into the system table that tracks the replication state of each table, and into the process list.

β=> CREATE SUBSCRIPTION t123_sub
CONNECTION 'host=localhost port=5432 user=student password=student dbname=replica_logical'
PUBLICATION t123_pub;
NOTICE:  created replication slot "t123_sub" on publisher
CREATE SUBSCRIPTION
β=> SELECT *, srrelid::regclass::text tablename FROM pg_subscription_rel;
 srsubid | srrelid | srsubstate | srsublsn  | tablename
---------+---------+------------+-----------+-----------
   16419 |   16411 | r          | 0/3084198 | test
   16435 |   16430 | i          |           | t3
   16435 |   16420 | i          |           | t1
   16435 |   16425 | i          |           | t2
(4 rows)

Here, it says that the table test is being replicated (r), and that the three new tables are still synchronizing.

student$ ps f -C postgres -o pid,command | grep beta
  82278 /usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/beta -c
config_file=/etc/postgresql/16/beta/postgresql.conf
  82279  \_ postgres: 16/beta: checkpointer
  82280  \_ postgres: 16/beta: background writer
  82283  \_ postgres: 16/beta: walwriter
  82284  \_ postgres: 16/beta: autovacuum launcher
  82285  \_ postgres: 16/beta: logical replication launcher
  82410  \_ postgres: 16/beta: student replica_logical [local] idle
  82645  \_ postgres: 16/beta: logical replication apply worker for subscription 16419
  83332  \_ postgres: 16/beta: logical replication apply worker for subscription 16435
  83353  \_ postgres: 16/beta: logical replication worker for subscription 16435 sync
16430
  83356  \_ postgres: 16/beta: logical replication worker for subscription 16435 sync
16420

In the process list, we see a new process apply worker serving the new subscription, as well as two synchronization processes. There are three tables, but the number of concurrent processes is limited by the parameter:

Page 14: Page 14

Source slide page 14: Page 14{#source-figure-014 caption=“Source slide page 14: Page 14”}

β=> SHOW max_sync_workers_per_subscription;
 max_sync_workers_per_subscription
-----------------------------------
 2
(1 row)

Wait for synchronization to complete. Now all the tables are being replicated.

β=> SELECT * FROM pg_subscription_rel;
 srsubid | srrelid | srsubstate |  srsublsn
---------+---------+------------+------------
   16419 |   16411 | r          | 0/3084198
   16435 |   16430 | r          | 0/12D22528
   16435 |   16420 | r          | 0/12D22528
   16435 |   16425 | r          | 0/12D22598
(4 rows)
β=> SELECT count(*) FROM t1;
  count
---------
 1000000
(1 row)

We don’t need the publication and subscription anymore, delete them:

β=> DROP SUBSCRIPTION t123_sub;
NOTICE:  dropped replication slot "t123_sub" on publisher
DROP SUBSCRIPTION
α=> DROP PUBLICATION t123_pub;
DROP PUBLICATION

Page 15: Row Identification

There must be a suitable row for change and delete operations

  • Logical row ID
ALTER TABLE ... REPLICA IDENTITY ...
  • DEFAULT
  • primary key columns (default)
USING INDEX
unique index columns with NOT NULL constraint
  • FULL
  • all columns
  • NOTHING
  • no identification
  • (default for the system catalog)

Speaker notes

New rows are inserted unambiguously on the subscription side. Changes and deletions are more complicated. You need to determine which row on the subscriber to apply the operation to. Therefore, a logical row ID is defined for each table. By default, rows are identified by primary key columns, but the identifier can be changed with ALTER TABLE … REPLICA IDENTITY.

Rows can be identified by another unique index, in which all columns have a NOT NULL constraint.

Another option is identification by all columns. In PostgreSQL 16 and higher, you can use full B-tree index lookup, where the first field is a column from the publication. PostgreSQL 17 and up can also use hash indexes. If there is no index available, a full scan is performed, which is extremely ineffective for large tables.

You can also completely disable replication support for certain tables (this is how system catalog tables are set up by default).

When changing or deleting, the column values of the tuple to be deleted do not get into the replica WAL – this is one of the reasons why the logical WAL level is required on the publishing server. At logical, it stores the old values of the columns included in the logical identifier. This is why with identification by all columns, the WAL size can be significantly larger compared to the replica level.

https://postgrespro.com/docs/postgresql/16/sql-altertable#SQL-ALTERTABL E-REPLICA-IDENTITY

Page 16: Row Filtering

Filter at publication

  • Filtering possible
    • insert
    • for any published columns
    • delete
    • for logical ID columns
    • update
    • for logical ID columns
    • this may transform into insert or delete
    • truncate
    • filter ignored
  • Plain expressions only

Speaker notes

By default, the publishing server sends out all published table rows.

Since PostgreSQL 15, you can specify a filter expression. Rows that evaluate as false or NULL will not be published.

Filtering does not affect the truncate operation. For insert operations, any published columns can be included in the filter expression, and for update and delete, only the columns included in the logical row ID.

For update, the filter expression is evaluated for both the old and the new row version. If the filter expression is false or NULL for both, the row is not replicated. If the old row doesn’t match the filter but the new one does, the new row is inserted. If the opposite is true — the old one matches but the new one does not — the row is deleted. If both match, update goes through.

You can only filter using plain expressions: no user-defined functions, operators, types and collations, no referencing system columns, no calling built-in functions with volatile and stable variability.

https://postgrespro.com/docs/postgresql/16/logical-replication-row-filter.html

Page 17: Row Identification and Filtering

By default, the logical row ID is determined by the primary key columns:

α=> SELECT relreplident FROM pg_class WHERE relname = 'test';
 relreplident
--------------
 d
(1 row)

d – default (primary key, if present) i – index f – all columns n – none Let’s say we need to replicate only rows where the cluster_name field has the value alpha. The cluster_name column is used in the filter expression, but it’s not published yet. Publish all the columns, without listing any specific ones.

α=> ALTER PUBLICATION test_pub SET TABLE test WHERE (cluster_name = 'alpha');
ALTER PUBLICATION
α=> \dRp+
                           Publication test_pub
  Owner  | All tables | Inserts | Updates | Deletes | Truncates | Via root
---------+------------+---------+---------+---------+-----------+----------
 student | f          | t       | t       | t       | t         | f
Tables:
    "public.test" WHERE (cluster_name = 'alpha'::text)

Observe how change replication works.

α=> INSERT INTO test(descr) VALUES ('Three');
INSERT 0 1
α=> INSERT INTO test(descr, cluster_name) VALUES ('Four', 'gamma');
INSERT 0 1
α=> SELECT * FROM test;
 id | descr | cluster_name
----+-------+--------------
  1 | One   | alpha
  2 | Two   | alpha
  3 | Three | alpha
  4 | Four  | gamma
(4 rows)
β=> SELECT * FROM test;
 id | descr | cluster_name | additional
----+-------+--------------+------------
  1 | One   | alpha        |
  2 | Two   | beta         |
  3 | Three | alpha        |
(3 rows)

The row with the value gamma was not replicated. Let’s try to modify the row.

α=> UPDATE test SET cluster_name = 'delta' WHERE id = 2;
ERROR:  cannot update table "test"
DETAIL:  Column used in the publication WHERE expression is not part of the replica
identity.

For upgrade and delete, the filter expression accepts only columns contained in the logical row ID, but for insert, all colums are good. Let’s change the filter condition.

Page 18: Page 18

α=> ALTER PUBLICATION test_pub SET TABLE test WHERE (id > 0);
ALTER PUBLICATION

Now modify the row:

α=> UPDATE test SET cluster_name = 'delta' WHERE id = 2;
UPDATE 1
β=> SELECT * FROM test WHERE id = 2;
 id | descr | cluster_name | additional
----+-------+--------------+------------
  2 | Two   | delta        |
(1 row)

The row change has been replicated. The new filter condition works:

α=> INSERT INTO test(id, descr) VALUES (0, 'Zero');
INSERT 0 1
α=> SELECT * FROM test ORDER BY id;
 id | descr | cluster_name
----+-------+--------------
  0 | Zero  | alpha
  1 | One   | alpha
  2 | Two   | delta
  3 | Three | alpha
  4 | Four  | gamma
(5 rows)
β=> SELECT * FROM test ORDER BY id;
 id | descr | cluster_name | additional
----+-------+--------------+------------
  1 | One   | alpha        |
  2 | Two   | delta        |
  3 | Three | alpha        |
(3 rows)

The row with id = 0 has not been published, so it wasn’t replicated.

Page 19: Logical Decoding

Source slide page 19: Logical Decoding{#source-figure-019 caption=“Source slide page 19: Logical Decoding”}

Speaker notes

During streaming physical replication, the wal sender process reads WAL records and passes them to the replica unchanged.

During logical replication, records are converted. The logical decoding module selects records that correspond to the published operations and tables, taking into account the columns and filter conditions. Selected records are converted to logical operations information, which is sent to the reorder buffer and stored there in accordance with the transaction IDs.

At the logical level, WAL additionally records the information required for logical decoding, in particular:

● for update – new values of all columns, not just changed ones,

● for update and delete – the old values of the columns in the logical ID,

● for commit – database OID.

The reorder buffer is in the local memory of the wal sender process. If the amount of data in it approaches the value of the logical_decoding_work_mem parameter, the largest transaction is sent to the replication slot (a file in PGDATA/pg_replslots/name).

When a transaction is committed, all its operations are sent to the output module, which converts them into replication protocol messages.

https://postgrespro.com/docs/postgresql/16/logicaldecoding

https://postgrespro.com/docs/postgresql/16/runtime-config-resource#GUC-L OGICAL-DECODING-WORK-MEM

Page 20: Slots and Logical Decoding

Let’s clear the table and check out logical decoding.

α=> TRUNCATE test;
TRUNCATE TABLE
β=> SELECT * FROM test ORDER BY id;
 id | descr | cluster_name | additional
----+-------+--------------+------------
(0 rows)

First, manually create a logical replication slot. The pgoutput plugin is used to send changes to the subscriber, but for observation purposes, the test_decoding plugin is more convenient:

α=> SELECT pg_create_logical_replication_slot('test_slot','test_decoding');
 pg_create_logical_replication_slot
------------------------------------
 (test_slot,0/12D2C990)
(1 row)

Now there are two slots on the alpha server:

α=> SELECT slot_name, plugin, slot_type, active FROM pg_replication_slots;
 slot_name |    plugin     | slot_type | active
-----------+---------------+-----------+--------
 test_sub  | pgoutput      | logical   | t
 test_slot | test_decoding | logical   | f
(2 rows)

In a separate session, start a transaction and insert a row into the table:

student$ psql -d replica_logical
α=> BEGIN;
BEGIN
α=> INSERT INTO test(descr) VALUES ('Πέντε');
INSERT 0 1

The output plugin can request changes from the slot:

α=> SELECT * FROM pg_logical_slot_get_changes('test_slot', NULL, NULL);
 lsn | xid | data
-----+-----+------
(0 rows)

The transaction isn’t complete yet, so the output plugin hasn’t received anything. Let’s make another change and commit the transaction.

α=> UPDATE test SET descr = 'Five' WHERE id = 5;
UPDATE 1
α=> UPDATE test SET id = 1, descr='One' WHERE id = 5;
UPDATE 1
α=> COMMIT;
COMMIT
α=> SELECT * FROM pg_logical_slot_get_changes('test_slot', NULL, NULL) \gx (xheader_width=90)

Page 21: Page 21

Source slide page 21: Page 21{#source-figure-021 caption=“Source slide page 21: Page 21”}

-[ RECORD 1 ]-----------------------------------------------------------------------------
lsn  | 0/12D44FA8
xid  | 766
data | BEGIN 766
-[ RECORD 2 ]-----------------------------------------------------------------------------
lsn  | 0/12D44FA8
xid  | 766
data | table public.test: INSERT: id[integer]:5 descr[text]:'Πέντε'
cluster_name[text]:'alpha'
-[ RECORD 3 ]-----------------------------------------------------------------------------
lsn  | 0/12D4CC78
xid  | 766
data | table public.test: UPDATE: id[integer]:5 descr[text]:'Five'
cluster_name[text]:'alpha'
-[ RECORD 4 ]-----------------------------------------------------------------------------
lsn  | 0/12D4EEE8
xid  | 766
data | table public.test: UPDATE: old-key: id[integer]:5 new-tuple: id[integer]:1
descr[text]:'One' cluster_name[text]:'alpha'
-[ RECORD 5 ]-----------------------------------------------------------------------------
lsn  | 0/12D51810
xid  | 766
data | COMMIT 766

Now the output plugin has received the changes. The subscriber collects the published tables. When the logical ID changes, its old value is sent too. Let’s remove the test slot, otherwise it will prevent WAL file segments from being deleted.

α=> SELECT pg_drop_replication_slot('test_slot');
 pg_drop_replication_slot
--------------------------
(1 row)
α=> \q

Page 22: Large Transactions

Source slide page 22: Large Transactions{#source-figure-022 caption=“Source slide page 22: Large Transactions”}

Speaker notes

By default (subscription streaming = off), changes are accumulated in the reorder buffer and sent to the subscriber after the transaction is committed.

If a transaction changes a lot of data, problems can occur:

● wal sender’s reorder buffer memory costs,

● writing files when the reorder buffer exceeds (logical_decoding_work_mem) memory,

● replica lag. If you set streaming = on, the publishing server will pass data changes to the subscriber while the transaction is still running. When the total amount of changes exceeds the value of the logical_decoding_work_mem parameter, the accumulated changes of the largest transaction in terms of volume are not dumped into to a file, but are immediately sent to the output module.

The subscriber saves these changes in its temporary files and applies them when committing the transaction.

Starting with PostgreSQL 16, if streaming = parallel and there are workers available, changes are not saved at the subscriber, but are immediately applied by the free workers. The number of such processes per subscription is limited by the max_parallel_apply_workers_per_subscription parameter.

https://postgrespro.com/docs/postgresql/16/sql-createsubscription#SQL-CR EATESUBSCRIPTION-WITH-STREAMING

https://postgrespro.com/docs/postgresql/16/runtime-config-replication#GUC-MAX-PARALLEL-APPLY-WORKERS-PER-SUBSCRIPTION

Page 23: Conflicts

Lead to errors

  • constraint violations,
  • insufficient rights to target table or row level security
  • replication error,
  • subscription with disable_on_error goes down
  • you can manually correct subscription-side data
  • or skip the conflicting transaction
  • Ignored
    • row being updated or deleted does not exist at the subscriber
    • replication continues, but consistency may be compromised

Speaker notes

Because the tables on the publishing server and at the subscriber can change independently of each other, a subscription operation may conflict with existing data.

Often a conflict is a situation where an operation cannot be performed, usually due to a violation of an integrity constraint, or the lack of permissions on the target table, or row level security restrictions.

In this case, the operation fails. The subscriber periodically repeats attempts, but they will fail until the conflict is resolved. Conflicts can only be resolved manually, automatic resolution has not yet been implemented. There are two options: fix the data at the subscriber or skip the conflicting transaction. In the second case, you need to disable replication, call the pg_replication_origin_advance function or execute the ALTER SUBSCRIPTION … SKIP…, and enable replication again. You can also pre-configure automatic disabling of replication by specifying the disable_on_error subscription option.

If the row to be modified or deleted does not exist at the subscriber, the operation will fail. In this case, no error occurs, but such ignored conflicts indicate that the data consistency between the publishing server and the subscriber is compromised.

https://postgrespro.com/docs/postgresql/16/logical-replication-conflicts

Page 24: Conflicts

Source slide page 24: Conflicts{#source-figure-024 caption=“Source slide page 24: Conflicts”}

Note

Note Note that sequences are not replicated. The second server created its own sequence:

β=> INSERT INTO test(descr) VALUES ('Six - local');
ERROR:  duplicate key value violates unique constraint "test_pkey"
DETAIL:  Key (id)=(1) already exists.

And this will work:

β=> INSERT INTO test VALUES (6, 'Six - local');
INSERT 0 1

What will happen if a value with the same key (6) appears on the publishing server?

α=> INSERT INTO test(descr) VALUES ('Six');
INSERT 0 1
α=> INSERT INTO test(descr) VALUES ('Seven');
INSERT 0 1

A conflict will occur during replication, and the replication will be paused.

β=> SELECT * FROM test ORDER BY id;
 id |    descr    | cluster_name | additional
----+-------------+--------------+------------
  1 | One         | alpha        |
  6 | Six - local | beta         |
(2 rows)

Technically, the logical replication apply worker process will continuously restart and check if the conflict is resolved. This resets the pg_stat_subscription information:

β=> SELECT * FROM pg_stat_subscription \gx
-[ RECORD 1 ]---------+---------
subid                 | 16419
subname               | test_sub
pid                   |
leader_pid            |
relid                 |
received_lsn          |
last_msg_send_time    |
last_msg_receipt_time |
latest_end_lsn        |
latest_end_time       |

The log will contain records about constraint violations:

student$ sudo tail -n 3 /var/log/postgresql/postgresql-16-beta.log
2026-09-17 00:03:37.210 MSK [85757] DETAIL:  Key (id)=(6) already exists.
2026-09-17 00:03:37.210 MSK [85757] CONTEXT:  processing remote data for replication
origin "pg_16419" during message type "INSERT" for replication target relation
"public.test" in transaction 767, finished at 0/12D6E280
2026-09-17 00:03:37.224 MSK [82278] LOG:  background worker "logical replication worker"
(PID 85757) exited with exit code 1

And the error counter in pg_stat_subscription_stats will increment with each restart:

β=> SELECT * FROM pg_stat_subscription_stats;
 subid | subname  | apply_error_count | sync_error_count | stats_reset
-------+----------+-------------------+------------------+-------------
 16419 | test_sub |                 2 |                0 |
(1 row)

(PostgreSQL 18 has introduced separate counters for each conflict type.) To resolve this conflict, delete the conflicting row from the table and wait a moment:

β=> DELETE FROM test WHERE id = 6;

Page 25: Page 25

DELETE 1

Check:

β=> SELECT * FROM test ORDER BY id;
 id | descr | cluster_name | additional
----+-------+--------------+------------
  1 | One   | alpha        |
  6 | Six   | alpha        |
  7 | Seven | alpha        |
(3 rows)

The data has appeared, replication has been restored.

Skipping a Conflicting Transaction

Another option is to skip the conflicting transaction entirely. The conflicting transaction:

β=> INSERT INTO test(id,descr) VALUES (8,'Eight - local');
INSERT 0 1
α=> INSERT INTO test(descr) VALUES ('Eight');
INSERT 0 1

The subscription has to be stoped before skipping the transaction:

β=> ALTER SUBSCRIPTION test_sub DISABLE;
ALTER SUBSCRIPTION

Instead of stopping manually, you can set the disable_on_error subscription parameter. Subscription stopped:

β=> \dRs
           List of subscriptions
   Name   |  Owner  | Enabled | Publication
----------+---------+---------+-------------
 test_sub | student | f       | {test_pub}
(1 row)

Check the log:

β=> \! tail -4 /var/log/postgresql/postgresql-16-beta.log
2026-09-17 00:03:43.408 MSK [86237] ERROR:  duplicate key value violates unique
constraint "test_pkey"
2026-09-17 00:03:43.408 MSK [86237] DETAIL:  Key (id)=(8) already exists.
2026-09-17 00:03:43.408 MSK [86237] CONTEXT:  processing remote data for replication
origin "pg_16419" during message type "INSERT" for replication target relation
"public.test" in transaction 771, finished at 0/12E27298
2026-09-17 00:03:43.421 MSK [82278] LOG:  background worker "logical replication worker"
(PID 86237) exited with exit code 1

The message contains the LSN of the conflicting transaction commit record. Skip the transaction:

β=> ALTER SUBSCRIPTION test_sub SKIP (lsn = '0/12E27298');
ALTER SUBSCRIPTION

A superuser could use the function pg_replication_origin_advance instead. Let’s enable replication.

β=> ALTER SUBSCRIPTION test_sub ENABLE;
ALTER SUBSCRIPTION
β=> SELECT * FROM pg_stat_subscription \gx

Page 26: Page 26

-[ RECORD 1 ]---------+------------------------------
subid                 | 16419
subname               | test_sub
pid                   | 86738
leader_pid            |
relid                 |
received_lsn          | 0/12E27010
last_msg_send_time    | 2026-09-17 00:03:47.222544+03
last_msg_receipt_time | 2026-09-17 00:03:47.224331+03
latest_end_lsn        | 0/12E27010
latest_end_time       | 2026-09-17 00:03:47.222544+03

Replication is working, but since we skipped the transaction, the data is now inconsistent:

α=> SELECT * FROM test ORDER BY id;
 id | descr | cluster_name
----+-------+--------------
  1 | One   | alpha
  6 | Six   | alpha
  7 | Seven | alpha
  8 | Eight | alpha
(4 rows)
β=> SELECT * FROM test ORDER BY id;
 id |     descr     | cluster_name | additional
----+---------------+--------------+------------
  1 | One           | alpha        |
  6 | Six           | alpha        |
  7 | Seven         | alpha        |
  8 | Eight - local | beta         |
(4 rows)

Clear the table:

α=> TRUNCATE test;
TRUNCATE TABLE

Triggers on the Subscriber

Triggers can execute on the subscriber, but by default a trigger will fire on local changes only and never on replication. This is useful when both servers have identical tables with the same set of triggers – in that case, the trigger has already fired on the publishing server and doesn’t need to run again on the subscriber. Let’s try it.

β=> CREATE OR REPLACE FUNCTION change_descr() RETURNS trigger AS $$
BEGIN
  NEW.additional := 'from the publication';
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION
β=> CREATE TRIGGER test_before_row
BEFORE INSERT OR UPDATE ON test
FOR EACH ROW
EXECUTE FUNCTION change_descr();
CREATE TRIGGER
α=> INSERT INTO test(descr) VALUES ('Nine');
INSERT 0 1
β=> SELECT * FROM test ORDER BY id;
 id | descr | cluster_name | additional
----+-------+--------------+------------
  9 | Nine  | alpha        |
(1 row)

The trigger didn’t fire. You can alter the table so the trigger fires only during replication:

Page 27: Page 27

Source slide page 27: Page 27{#source-figure-027 caption=“Source slide page 27: Page 27”}

β=> ALTER TABLE test ENABLE REPLICA TRIGGER test_before_row;
ALTER TABLE

Or in both cases: during replication and during local changes.

β=> ALTER TABLE test ENABLE ALWAYS TRIGGER test_before_row;
ALTER TABLE

You can distinguish between these situations using the session_replication_role parameter:

β=> CREATE OR REPLACE FUNCTION change_descr() RETURNS trigger AS $$
BEGIN
  NEW.additional := current_setting('session_replication_role');
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Add one row on each server:

α=> INSERT INTO test(descr) VALUES ('Ten');
INSERT 0 1
β=> INSERT INTO test(id, descr) VALUES (11, 'Eleven');
INSERT 0 1
β=> SELECT * FROM test ORDER BY id;
 id | descr  | cluster_name | additional
----+--------+--------------+------------
  9 | Nine   | alpha        |
 10 | Ten    | alpha        | replica
 11 | Eleven | beta         | origin
(3 rows)

The trigger fired in both cases.

Page 28: Replication origin

Source slide page 28: Replication origin{#source-figure-028 caption=“Source slide page 28: Replication origin”}

origin subscription option

  • any — transmit all changes (default)
  • none — transmit local changes only

Speaker notes

If you define both a publication and a subscription on the same server and transfer all changes, the replicated changes will be replicated, and things will snowball.

Starting with PostgreSQL 16, when the publishing server starts a transaction that was initially received from another node, it sends a message about the origin of this transaction first. This is also important for cascading replication.

https://postgrespro.com/docs/postgresql/16/protocol-logical-replication#PRO TOCOL-LOGICAL-MESSAGES-FLOW

By default, the subscription starts with the origin = any parameter and the publication passes all transactions to it: both local and received from another node.

However, in topologies where loops can occur, such as with bidirectional replication, you should set origin = none. Then the publication will transfer only local changes.

https://postgrespro.com/docs/postgresql/16/sql-createsubscription#SQL-CR EATESUBSCRIPTION-WITH-STREAMING

Information about the origins is stored in the pg_replication_origin view.

https://postgrespro.com/docs/postgresql/16/catalog-pg-replication-origin#CA TALOG-PG-REPLICATION-ORIGIN

The current replay position for a specific replication origin is displayed in the pg_replication_origin_status view.

https://postgrespro.com/docs/postgresql/16/view-pg-replication-origin-status #VIEW-PG-REPLICATION-ORIGIN-STATUS

Page 29: Replication origin

Delete rows from the table:

α=> TRUNCATE test;
TRUNCATE TABLE

Publish several columns from table test on server beta.

β=> CREATE PUBLICATION test_pub FOR TABLE test(id, descr, cluster_name);
CREATE PUBLICATION

Now the publications exist on both servers. If you create a subscription of alpha, it will receive all changes from beta, including those sent by alpha’s publication. But alpha cannot apply these changes because of integrity constraints, so replication will stop. For beta to receive just the local changes from alpha, set the origin subscription parameter:

β=> ALTER SUBSCRIPTION test_sub SET (origin = none);
ALTER SUBSCRIPTION

For the changes from replication, the origin will be non-empty, and the publication will not send them to the subscriber. Now let’s make alpha a subscriber to beta. Disable initial synchronization, as the table is empty. Use the subscription parameter origin to receive only the changes made on beta:

α=> CREATE SUBSCRIPTION test_sub
CONNECTION 'host=localhost port=5433 user=student password=student dbname=replica_logical'
PUBLICATION test_pub WITH (copy_data = false, origin = none);
NOTICE:  created replication slot "test_sub" on publisher
CREATE SUBSCRIPTION

Now there are subscriptions on both servers.

α=> \dRs
           List of subscriptions
   Name   |  Owner  | Enabled | Publication
----------+---------+---------+-------------
 test_sub | student | t       | {test_pub}
(1 row)
β=> \dRs
           List of subscriptions
   Name   |  Owner  | Enabled | Publication
----------+---------+---------+-------------
 test_sub | student | t       | {test_pub}
(1 row)

Insert a row on alpha.

α=> INSERT INTO test VALUES (1, 'One potato');
INSERT 0 1

Check beta.

β=> SELECT * FROM test;
 id |   descr    | cluster_name | additional
----+------------+--------------+------------
  1 | One potato | alpha        | replica
(1 row)

Insert a row on beta.

β=> INSERT INTO test VALUES (2, 'Two potato');
INSERT 0 1

Check alpha.

α=> SELECT * FROM test;

Page 30: Page 30

 id |   descr    | cluster_name
----+------------+--------------
  1 | One potato | alpha
  2 | Two potato | beta
(2 rows)

Verify the changes.

α=> UPDATE test SET descr = 'Three potato, four';
UPDATE 2
α=> SELECT * FROM test;
 id |       descr        | cluster_name
----+--------------------+--------------
  1 | Three potato, four | alpha
  2 | Three potato, four | beta
(2 rows)
β=> SELECT * FROM test;
 id |       descr        | cluster_name | additional
----+--------------------+--------------+------------
  1 | Three potato, four | alpha        | replica
  2 | Three potato, four | beta         | replica
(2 rows)

If replication is no longer needed, the subscription must be dropped – otherwise, the replication slots will remain open on the publishing server.

α=> DROP SUBSCRIPTION test_sub;
NOTICE:  dropped replication slot "test_sub" on publisher
DROP SUBSCRIPTION
β=> DROP SUBSCRIPTION test_sub;
NOTICE:  dropped replication slot "test_sub" on publisher
DROP SUBSCRIPTION

Page 31: Takeaways

Logical replication uses the publish-subscribe pattern.

  • Row-level changes are replicated
  • Rows are matched using logical identifiers
  • Data mismatch on different servers may cause conflicts
  • Origin information forwarding helps track the progress of cascade replication and eliminate loops

Page 32: Practice

  1. Create two databases on a single server.
  • In the first database, create a table with a primary key and insert a few rows into it. Set up logical replication for the table from the first database to the second, verify that replication is working, and then remove the subscription. 2. Configure logical replication of the table from alpha to beta. Execute a transaction that modifies a large amount of data, and before committing it, verify that the data has been written to disk. Repeat the same steps, but this time in streaming mode. Did the data get to disk?

Speaker notes

  1. Use the pg_dump tool with the –schema-only option.

If you try the regular way, the subscription creation command will hang because it needs to wait for all active transactions on the publishing server to complete, including itself. In this case, you must create a logical replication slot in advance, as described in the documentation: https://postgrespro.com/docs/postgresql/16/sql-createsubscription

  1. To prevent the publication from getting into the logical copy, use the –no-publications option.

Check the value of the logical_decoding_work_mem configuration parameter, which sets the maximum size of the reorder buffer. If this value is exceeded, data will be pushed to disk.

Page 33: 1. Logical Replication on a Single Server

Set the WAL level to logical first.

α=> ALTER SYSTEM SET wal_level = logical;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

Databases:

student$ psql
α=> CREATE DATABASE replica_logical;
CREATE DATABASE
α=> CREATE DATABASE replica_logical_bis;
CREATE DATABASE

Table in the first database:

α=> \c replica_logical
You are now connected to database "replica_logical" as user "student".
α=> CREATE TABLE test(
  id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
  descr text
);
CREATE TABLE
α=> INSERT INTO test(descr) VALUES ('One'), ('Two'), ('Three');
INSERT 0 3

Using logical backup to transfer a schema is especially convenient when there are many tables.

student$ pg_dump --no-acl --schema-only replica_logical | psql -d replica_logical_bis
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
SET
CREATE TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE

Transferring data with pg_dump makes no sense here, since the tables may change during the transfer.

student$ psql -d replica_logical_bis
α=> \d test
                            Table "public.test"
 Column |  Type   | Collation | Nullable |             Default
--------+---------+-----------+----------+----------------------------------
 id     | integer |           | not null | generated by default as identity
 descr  | text    |           |          |
Indexes:
    "test_pkey" PRIMARY KEY, btree (id)

Publication:

Page 34: Page 34

α=> CREATE PUBLICATION test_pub FOR TABLE test;
CREATE PUBLICATION

Since replication will be configured on the same server, a logical replication slot must be created first.

α=> SELECT pg_create_logical_replication_slot('testslot','pgoutput');
 pg_create_logical_replication_slot
------------------------------------
 (testslot,0/23EEC58)
(1 row)

Then create the subscription:

α=> CREATE SUBSCRIPTION test_sub
CONNECTION 'user=student password=student dbname=replica_logical host=localhost'
PUBLICATION test_pub
WITH (create_slot = false, slot_name = testslot);
CREATE SUBSCRIPTION

Verify that replication is working:

α=> INSERT INTO test(descr) VALUES ('Four');
INSERT 0 1
α=> SELECT * FROM test;
 id | descr
----+-------
  1 | One
  2 | Two
  3 | Three
  4 | Four
(4 rows)

Drop the subscription and the slot.

α=> DROP SUBSCRIPTION test_sub;
NOTICE:  dropped replication slot "testslot" on publisher
DROP SUBSCRIPTION
α=> \q

2. Logical Replication from Alpha to Beta

Start server beta and copy the DDL commands for the test table. Publication creation commands are not required on the subscriber.

student$ sudo pg_ctlcluster 16 beta start
student$ pg_dump \
  --create \
  --schema-only \
  --no-publications \
  --no-acl \
  --dbname=replica_logical \
  | psql -p 5433
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
CREATE DATABASE
ALTER DATABASE
You are now connected to database "replica_logical" as user "student".
SET
SET

Page 35: Page 35

SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
SET
CREATE TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE

Create a subscription on beta – the slot on alpha is created automatically.

student$ psql -p 5433 -d replica_logical
β=> CREATE SUBSCRIPTION test_sub
CONNECTION 'user=student password=student dbname=replica_logical host=localhost'
PUBLICATION test_pub;
NOTICE:  created replication slot "test_sub" on publisher
CREATE SUBSCRIPTION
β=> SELECT count(*) FROM test;
 count
-------
     4
(1 row)

Replication is working. Truncate the test table on the publication server (alpha):

α=> TRUNCATE test;
TRUNCATE TABLE
β=> SELECT count(*) FROM test;
 count
-------
     0
(1 row)

The subscriber received the changes. Start a transaction that inserts a large number of rows into the table.

α=> BEGIN;
BEGIN
α=> INSERT INTO test(descr)
SELECT now()::text FROM generate_series(1,1_000_000) AS g(id);
INSERT 0 1000000

The logical_decoding_work_mem parameter defines the maximum size of the reordering buffer.

α=> SHOW logical_decoding_work_mem;
 logical_decoding_work_mem
---------------------------
 64MB
(1 row)

When this limit is exceeded, data is spilled to disk in the PGDATA/pg_replslot/slot-name directory. How much disk space does the directory take up?

postgres$ du -sh /var/lib/postgresql/16/alpha/pg_replslot/test_sub
135M
/var/lib/postgresql/16/alpha/pg_replslot/test_sub

Page 36: Page 36

By default, no data is sent to the subscriber until the transaction is committed.

β=> SELECT count(*) FROM test;
 count
-------
     0
(1 row)

Commit the transaction…

α=> COMMIT;
COMMIT

…and wait a moment: The subscriber received all the rows:

β=> SELECT count(*) FROM test;
  count
---------
 1000000
(1 row)

And the data spilled to disk has been removed:

postgres$ du -sh /var/lib/postgresql/16/alpha/pg_replslot/test_sub
8.0K
/var/lib/postgresql/16/alpha/pg_replslot/test_sub

Check pg_stat_replication_slots:

α=> SELECT * FROM pg_stat_replication_slots \gx
-[ RECORD 1 ]+----------
slot_name    | test_sub
spill_txns   | 1
spill_count  | 3
spill_bytes  | 162000000
stream_txns  | 0
stream_count | 0
stream_bytes | 0
total_txns   | 2
total_bytes  | 162001984
stats_reset  |

spill_txns – the number of transactions spilled to disk spill_count – the number of times data was spilled to disk due to exceeding logical_decoding_work_mem spill_bytes – the number of bytes spilled to disk Repeat the experiment in streaming mode. Reset the statistics and truncate the table again.

α=> SELECT pg_stat_reset_replication_slot('test_sub');
 pg_stat_reset_replication_slot
--------------------------------
(1 row)
α=> TRUNCATE test;
TRUNCATE TABLE

Now enable streaming mode for the subscription:

β=> ALTER SUBSCRIPTION test_sub SET (streaming = on);
ALTER SUBSCRIPTION

Run the large transaction again.

α=> BEGIN;
BEGIN
α=> INSERT INTO test(descr)
SELECT now()::text FROM generate_series(1,1_000_000) AS g(id);

Page 37: Page 37

INSERT 0 1000000

Now there’s much less data in the slot’s directory – some of it has already been sent to the subscriber:

postgres$ du -sh /var/lib/postgresql/16/alpha/pg_replslot/test_sub
8.0K
/var/lib/postgresql/16/alpha/pg_replslot/test_sub

Commit the transaction…

α=> COMMIT;
COMMIT

…and wait a moment:

β=> SELECT count(*) FROM test;
  count
---------
 1000000
(1 row)

Check pg_stat_replication_slots:

α=> SELECT * FROM pg_stat_replication_slots \gx
-[ RECORD 1 ]+------------------------------
slot_name    | test_sub
spill_txns   | 0
spill_count  | 0
spill_bytes  | 0
stream_txns  | 1
stream_count | 3
stream_bytes | 161000000
total_txns   | 2
total_bytes  | 161001984
stats_reset  | 2026-09-17 00:17:52.013907+03

This time, when the allocated memory was exhausted, data was not spilled to disk but sent directly to the subscriber.

Cleanup

Drop the subscription, slot, and databases, and reset configuration.

β=> DROP SUBSCRIPTION test_sub;
NOTICE:  dropped replication slot "test_sub" on publisher
DROP SUBSCRIPTION
β=> \c postgres
You are now connected to database "postgres" as user "student".
β=> DROP DATABASE replica_logical;
DROP DATABASE
student$ sudo pg_ctlcluster 16 beta stop
α=> \c postgres
You are now connected to database "postgres" as user "student".
α=> DROP DATABASE replica_logical;
DROP DATABASE
α=> DROP DATABASE replica_logical_bis;
DROP DATABASE
α=> ALTER SYSTEM RESET wal_level;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

Page 38: Practice+

1. Create and publish a table with a bytea column on alpha. Insert several million rows into the table. Compare the initial subscription synchronization time in normal and binary modes using the beta server. 2. Enable concurrent synchronization for the subscription. By adding rows to the table, examine the subscription workers.

Speaker notes

  1. You can estimate the initial synchronization time by monitoring the pg_subscription_rel table. You can set it up on the client side using the \watch metacommand or on the server side using a function written in, for example, PL/pgSQL.

https://postgrespro.com/docs/postgresql/16/catalog-pg-subscription-rel#CAT ALOG-PG-SUBSCRIPTION-REL

https://postgrespro.ru/docs/postgresql/16/plpgsql

  1. Set an appropriate value for the streaming subscription option. Use the pg_stat_subscription view to monitor subscription processes.

Page 39: 1. Initial Data Synchronization for Logical Replication

Create a database.

α=> CREATE DATABASE replica_logical;
CREATE DATABASE
α=> \c replica_logical
You are now connected to database "replica_logical" as user "student".

Change the WAL level and restart the server.

α=> ALTER SYSTEM SET wal_level = logical;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart
student$ psql -d replica_logical

Create a table with a large number of rows.

α=> CREATE TABLE t AS
SELECT n::text::bytea
FROM generate_series(1, 5_000_000) AS g(n);
SELECT 5000000

Start the beta server and copy the database to it: with the table structure but without any data.

student$ sudo pg_ctlcluster 16 beta start
student$ pg_dump -d replica_logical --schema-only --create --no-acl | psql -p 5433
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
CREATE DATABASE
ALTER DATABASE
You are now connected to database "replica_logical" as user "student".
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
SET
CREATE TABLE
ALTER TABLE
student$ psql -p 5433 -d replica_logical

Create a publication on alpha.

α=> CREATE PUBLICATION pub FOR TABLE t;
CREATE PUBLICATION

Page 40: Page 40

Source slide page 40: Page 40{#source-figure-040 caption=“Source slide page 40: Page 40”}

The subscription on beta will initially be inactive.

β=> CREATE SUBSCRIPTION sub
CONNECTION 'dbname=replica_logical host=localhost port=5432 user=postgres password=postgres'
PUBLICATION pub
WITH (enabled = false);
NOTICE:  created replication slot "sub" on publisher
CREATE SUBSCRIPTION

The table’s status in the subscription is displayed in the following view:

β=> SELECT srsubid, srrelid::regclass, srsubstate, srsublsn
FROM pg_subscription_rel;
 srsubid | srrelid | srsubstate | srsublsn
---------+---------+------------+----------
   16397 | t       | i          |
(1 row)

Status codes in the srsubstate field: i – initialization d – data copy f – table copy complete s – synchronization complete r – normal replication We’ll measure the synchronization time using a PL/pgSQL function:

β=> CREATE OR REPLACE FUNCTION time4sync() RETURNS interval AS
$$
DECLARE
  st timestamp := clock_timestamp();
BEGIN
  WHILE EXISTS(SELECT 1 FROM pg_subscription_rel WHERE srsubstate <> 'r') LOOP
    PERFORM pg_sleep(0.01); -- достаточная точность
  END LOOP;
  RETURN clock_timestamp() - st;
END;
$$
LANGUAGE plpgsql;
CREATE FUNCTION

Activate the subscription and measure the initial synchronization time.

β=> ALTER SUBSCRIPTION sub ENABLE; SELECT time4sync();
ALTER SUBSCRIPTION
    time4sync
-----------------
 00:00:09.000456
(1 row)

Now let’s configure synchronization in binary mode. Drop the subscription, truncate the table, and create a new subscription with the binary parameter.

β=> DROP SUBSCRIPTION sub;
NOTICE:  dropped replication slot "sub" on publisher
DROP SUBSCRIPTION
β=> TRUNCATE TABLE t;
TRUNCATE TABLE
β=> CREATE SUBSCRIPTION sub
CONNECTION 'dbname=replica_logical host=localhost port=5432 user=postgres password=postgres'
PUBLICATION pub
WITH (enabled = false, binary = true);
NOTICE:  created replication slot "sub" on publisher
CREATE SUBSCRIPTION

Enable the subscription and measure the synchronization time.

β=> ALTER SUBSCRIPTION sub ENABLE; SELECT time4sync();

Page 41: Page 41

ALTER SUBSCRIPTION
    time4sync
-----------------
 00:00:07.282319
(1 row)

The initial synchronization is noticeably faster. The binary parameter also applies to subsequent synchronization. However, binary mode is not supported for all data types; portability across different architectures and major PostgreSQL versions must also be considered. Binary mode was introduced in PostgreSQL 14, and in versions prior to 16, initial synchronization always runs in text mode.

2. Parallel Application of Changes

Enable parallel mode for the subscription.

β=> ALTER SUBSCRIPTION sub SET (streaming = parallel);
ALTER SUBSCRIPTION

Processes serving the subscription:

β=> SELECT
  a.pid, a.leader_pid, a.backend_type,
  r.srsubstate, s.received_lsn
FROM pg_stat_subscription s
  LEFT JOIN pg_subscription_rel r ON r.srsubid=s.subid AND r.srrelid='t'::regclass
  LEFT JOIN pg_stat_activity a ON s.pid=a.pid
WHERE s.subname = 'sub';
  pid   | leader_pid |        backend_type        | srsubstate | received_lsn
--------+------------+----------------------------+------------+--------------
 114667 |            | logical replication worker | r          |
(1 row)

Truncate the table, insert a large number of rows, and re-run the query against the system catalog views:

α=> TRUNCATE t;
TRUNCATE TABLE
α=> INSERT INTO t SELECT g.i::text::bytea FROM generate_series(1, 1_000_000) AS g(i);
β=> SELECT
  a.pid, a.leader_pid, a.backend_type,
  r.srsubstate, s.received_lsn
FROM pg_stat_subscription s
  LEFT JOIN pg_subscription_rel r ON r.srsubid=s.subid AND r.srrelid='t'::regclass
  LEFT JOIN pg_stat_activity a ON s.pid=a.pid
WHERE s.subname = 'sub';
  pid   | leader_pid |            backend_type             | srsubstate | received_lsn
--------+------------+-------------------------------------+------------+--------------
 114667 |            | logical replication worker          | r          | 0/16FFFFE8
 114896 |     114667 | logical replication parallel worker | r          |
(2 rows)

An additional process has appeared that applies changes without waiting for the transaction to complete on the publisher.

INSERT 0 1000000

Row insertion is complete. Wait a little longer…

β=> select count(*) from t;
  count
---------
 1000000
(1 row)

The subscriber has now received all the rows.

Cleanup

Delete the subscription.

β=> DROP SUBSCRIPTION sub;

Page 42: Page 42

NOTICE:  dropped replication slot "sub" on publisher
DROP SUBSCRIPTION

Drop the databases.

β=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
β=> DROP DATABASE replica_logical;
DROP DATABASE

Reset the configuration.

α=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
α=> ALTER SYSTEM RESET synchronous_standby_names;
ALTER SYSTEM
α=> ALTER SYSTEM RESET wal_level;
ALTER SYSTEM

Restart the alpha server.

student$ sudo pg_ctlcluster 16 alpha restart
student$ psql
α=> DROP DATABASE replica_logical;
DROP DATABASE

Stop the beta server.

student$ sudo pg_ctlcluster 16 beta stop

07 Lesson 07: Replication / Use Cases

Native Markdown source page: Replication / Use Cases
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: No matching course-page link was captured in the source inventory. Archive: DBA3-handouts-16-en-20260917.zip PDF pages: 33

Page 1: Replication / Use Cases

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Alexey Beresnev Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Physical Replicaion Use Cases
  • Logical Replication Use Cases

Page 3: Physical Replication

Hot Standby for High Availability

  • Load Balancing for OLTP Workloads
  • Replica for Reporting
  • Multiple Replicas and Cascading Replication
  • Delayed Replication

Page 4: Hot Standby for High Availability

Source slide page 4: Hot Standby for High Availability{#source-figure-004 caption=“Source slide page 4: Hot Standby for High Availability”}

synchronous replication: the replica stays as closely aligned with the leader as possible queries to replica are possible, but not a priority: they will be terminated in case of any conflict

Speaker notes

A replica can be set up to serve different purposes, and its configuration depends on the intended use case.

One possible scenario is maintaining a hot standby for high availability purposes. This means that if the main server fails, we need to switch over to the replica as quickly as possible without losing any data.

Reliability is ensured by synchronous replication with synchronous_commit = on mode (write option does not guarantee reliability, apply may cause unnecessary delays).

For synchronous replication, the synchronous_standby_names configuration parameter must specify a list of standby servers (replicas).

To minimize the replica’s lag behind the primary server, WAL records should be applied immediately. We can set the max_standby_streaming_delay parameter to something small to achieve this.

To prevent queries on the replica from negatively impacting the primary server, we can disable feedback (hot_standby_feedback = off).

With these settings, queries on the replica are allowed, but they’ll be terminated if conflicts occur. If queries to the replica aren’t planned, you can set it up in warm standby mode with hot_standby = off.

Note that replication is a fundamental mechanism for ensuring high availability, but it’s just one of many features of HA systems. This is covered in more detail in the “Cluster Technologies” module.

Page 5: OLTP Load Balancing

Source slide page 5: OLTP Load Balancing{#source-figure-005 caption=“Source slide page 5: OLTP Load Balancing”}

queries on the replica must complete successfully long-running queries will impact the main server, but in an OLTP environment, they shouldn’t exist in the first place data consistency must be handled separately

Speaker notes

A replica can also be used to balance OLTP read load.

OLTP loads have a lot of short-running queries. This keeps the replica closely synchronized with the main server to ensure queries won’t be interrupted by conflicts, while avoiding any negative impact on the main server.

We can enable feedback (hot_standby_feedback = on) and set a sufficiently large delay for applying conflicting records (max_standby_streaming_delay).

Synchronous replication likely isn’t needed in this configuration. It won’t guarantee consistency anyway: a change committed on the leader will not necessarily be noticed by the replica right away.

To ensure consistency, changes must commit on both servers simultaneously. The synchronous_commit = apply mode allows waiting for the WAL record to be applied on the replica (at the cost of significantly degraded primary server performance), but then you could see the changes on the replica before they appear on the leader. A reliable solution that’s transparent to applications can be achieved using specialized distributed transaction protocols (see the “Cluster Technologies” module).

Page 6: Replica for Reporting

Source slide page 6: Replica for Reporting{#source-figure-006 caption=“Source slide page 6: Replica for Reporting”}

queries on replica must complete successfully, including long-running ones (but the data may not be completely up-to-date)

  • the leader shouldn’t be affected by feedback from the replica to completely decouple the servers, remove the replication slot and rely on just the WAL archive

Speaker notes

Another option is to distribute different types of workload across separate servers. The main server can handle OLTP workloads, while long-running read queries (reports) can be offloaded to a replica.

Beyond distributing workload across servers, this approach also prevents long-running queries on the main server from delaying vacuum operations and causing table bloat.

Based on these considerations, the reporting replica should be kept as isolated as possible from the main server. Feedback should be disabled (hot_standby_feedback = off), but the delay for conflicting WAL records should be set very high, up to infinity (max_standby_streaming_delay = −1).

Since long-running reports don’t need up-to-the-minute data, you can forgo using a replication slot if an archive is available (just remember to adjust the max_standby_archive_delay parameter the same way as max_standby_streaming_delay). In that case, if the leader removes a WAL file before the replica can retrieve it, the replica will fetch it from the archive instead.

This replica can also be used for creating backups.

Of course, other viable parameter combinations exist. The key is to have a clear understanding of what problem needs solving and how different parameters affect the outcome.

Page 7: Multiple replicas

Source slide page 7: Multiple replicas{#source-figure-007 caption=“Source slide page 7: Multiple replicas”}

Speaker notes

Multiple replicas can be connected to the main server. No special configuration is required for this, but keep in mind that each replica will have its own dedicated wal sender process and its own replication slot. During deployment, you can distribute a single base backup and clone the replication slot using the pg_copy_physical_replication_slot function.

https://postgrespro.com/docs/postgresql/16/functions-admin#FUNCTIONS-R EPLICATION

This setup is typically used for load distribution, but the actual distribution itself must be handled by external tools (see the “Cluster Technologies” module).

Page 8: Primary Server and Two Physical Replicas

Let’s set up a configuration with two replicas. Take a standalone backup of the first server while simultaneously creating a slot.

student$ pg_basebackup \
  --checkpoint=fast \
  --pgdata=/home/student/tmp/backup \
  -R \
  --create-slot --slot=beta

Since the third server will start applying WAL records from the same position as the second, we can duplicate the slot:

α=> SELECT pg_copy_physical_replication_slot('beta','gamma');
 pg_copy_physical_replication_slot
-----------------------------------
 (gamma,)
(1 row)

Deploy the standalone backup to the PG_DATA directories on the second and third servers:

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
student$ sudo rm -rf /var/lib/postgresql/16/beta
student$ sudo cp -r /home/student/tmp/backup /var/lib/postgresql/16/beta
student$ sudo chown -R postgres:postgres /var/lib/postgresql/16/beta
student$ sudo pg_ctlcluster 16 gamma stop
Cluster is not running.
student$ sudo rm -rf /var/lib/postgresql/16/gamma
student$ sudo cp -r /home/student/tmp/backup /var/lib/postgresql/16/gamma
student$ sudo chown -R postgres:postgres /var/lib/postgresql/16/gamma

In the third server’s configuration, set the gamma slot:

postgres$ sed 's/beta/gamma/g' -i /var/lib/postgresql/16/gamma/postgresql.auto.conf
postgres$ tail -n 1 /var/lib/postgresql/16/gamma/postgresql.auto.conf
primary_slot_name = 'gamma'

Start both replicas:

student$ sudo pg_ctlcluster 16 beta start
student$ sudo pg_ctlcluster 16 gamma start

The slots have been initialized:

α=> SELECT slot_name, active_pid, restart_lsn FROM pg_replication_slots;
 slot_name | active_pid | restart_lsn
-----------+------------+-------------
 beta      |      88448 | 0/30483F8
 gamma     |      88494 | 0/30483F8
(2 rows)

Test:

α=> CREATE DATABASE replica_usecases;
CREATE DATABASE
α=> \c replica_usecases
You are now connected to database "replica_usecases" as user "student".
α=> CREATE TABLE revenue(city text, amount numeric);
CREATE TABLE

Page 9: Page 9

Source slide page 9: Page 9{#source-figure-009 caption=“Source slide page 9: Page 9”}

student$ psql -p 5433
student$ psql -p 5434
β=> \c replica_usecases
You are now connected to database "replica_usecases" as user "student".
γ=> \c replica_usecases
You are now connected to database "replica_usecases" as user "student".
β=> \d revenue
              Table "public.revenue"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 city   | text    |           |          |
 amount | numeric |           |          |
γ=> \d revenue
              Table "public.revenue"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 city   | text    |           |          |
 amount | numeric |           |          |

We’ve set up two replicas of the same primary from a single base backup.

Page 10: Cascading replication

Source slide page 10: Cascading replication{#source-figure-010 caption=“Source slide page 10: Cascading replication”}

reduces load on the leader and redistributes network traffic cascaded synchronous replication is not supported feedback comes from all replicas

Speaker notes

Multiple replicas connected to a single primary server will generate additional load on it. Network load should also be considered when transmitting multiple copies of the WAL stream.

To reduce this load, replicas can be arranged in a cascade configuration, where servers relay WAL records to each other in a chain. The further downstream from the leader, the greater potential lag may accumulate in the replicated data. With this setup, the replication process has to be monitored at multiple servers at once, making monitoring more complicated.

When a switchover to a replica occurs (to the one closest to the primary server), its timeline ID increments. recovery_target_timeline = ’latest’ will direct the recovery to this new timeline (this is the default value).

Note that cascaded synchronous replication is not supported here – the leader can only synchronize with replicas directly connected to it. Therefore, on the first replica in the cascade, you need to set synchronous_standby_names = “or synchronous_commit = ’local'.

However, the leader collects feedback from all replicas in the cascade.

Page 11: Delayed Replication

Source slide page 11: Delayed Replication{#source-figure-011 caption=“Source slide page 11: Delayed Replication”}

a time machine with recovery options to multiple points in time, no archive needed

Speaker notes

Case: have the ability to view data at and recover to an arbitrary point in time.

Point-in-time recovery from archive already does precisely this, but it takes a while, and has to be set up in advance.

Alternatively, you can create a replica that applies WAL records with a delay (recovery_min_apply_delay). This requires clock synchronization between servers.

Only commit records are delayed, not all WAL records. Records prior to commit may be applied “ahead of schedule,” but this poses no problem thanks to multiversion concurrency control. Feedback should be disabled to prevent table bloat on the leader.

A typical scenario: something goes wrong on the leader (for instance, critical data gets deleted). The replica still has the data because the corresponding records haven’t been applied yet. Further WAL replay is paused using the pg_wal_replay_pause function while the issue is being investigated. The pg_get_wal_replay_pause_state function allows you to check the pause status.

To resume, use thepg_wal_replay_resume function to lift the pause. If you decide to roll back to a point before the deletion occurred, set the recovery target using one of the recovery_target_* parameters and restart the replica.

Page 12: Logical Replication

Consolidation and Shared Reference Data

  • Major Version Upgrade
  • Leader-Leader
  • Logical Decoding at Physical Replica

Page 13: Consolidation

Source slide page 13: Consolidation{#source-figure-013 caption=“Source slide page 13: Consolidation”}

collection and consolidation of data from regional servers

Speaker notes

Suppose there are several regional branches, each of which runs on its own PostgreSQL server. The goal is to consolidate some of the data on the central server.

First, publications for the necessary data are created on regional servers. The central server subscribes to these publications. The received data can be processed on the central server using triggers: for example, to standardize it into a uniform format.

Since logical replication relies on streaming data through replication slots, the servers need a reasonably stable connection: otherwise, during connection outages, the regional servers will have to retain WAL files.

This approach has many nuances from a business logic perspective that require thorough examination. In some cases, it may be easier to transmit data in batches every now and again.

Page 14: Shared reference data

Source slide page 14: Shared reference data{#source-figure-014 caption=“Source slide page 14: Shared reference data”}

reference data replication

Speaker notes

Another use case: the main server maintains reference data that needs to be kept current across regional servers.

In this case, the setup is reversed: the main server publishes changes, and regional servers subscribe to receive those updates.

Page 15: Consolidation Using Logical Replication

Source slide page 15: Consolidation Using Logical Replication{#source-figure-015 caption=“Source slide page 15: Consolidation Using Logical Replication”}

Promote both previously configured replicas and set up data consolidation.

student$ sudo pg_ctlcluster 16 beta promote
student$ sudo pg_ctlcluster 16 gamma promote
β=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 f
(1 row)
γ=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 f
(1 row)

Logical replication requires a higher WAL level (a restart will be needed).

β=> ALTER SYSTEM SET wal_level = 'logical';
ALTER SYSTEM
γ=> ALTER SYSTEM SET wal_level = 'logical';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta restart
student$ sudo pg_ctlcluster 16 gamma restart

Remove the replication slots on the alpha server:

α=> SELECT pg_drop_replication_slot('beta');
 pg_drop_replication_slot
--------------------------
(1 row)
α=> SELECT pg_drop_replication_slot('gamma');
 pg_drop_replication_slot
--------------------------
(1 row)

Publish the table on the second and third servers:

student$ psql -p 5433 -d replica_usecases
β=> CREATE PUBLICATION revenue FOR TABLE revenue;
CREATE PUBLICATION
student$ psql -p 5434 -d replica_usecases
γ=> CREATE PUBLICATION revenue FOR TABLE revenue;
CREATE PUBLICATION

The first server subscribes to both publications:

α=> CREATE SUBSCRIPTION msk
CONNECTION 'port=5433 dbname=replica_usecases host=localhost user=student password=student'
PUBLICATION revenue;
NOTICE:  created replication slot "msk" on publisher
CREATE SUBSCRIPTION
α=> CREATE SUBSCRIPTION spb
CONNECTION 'port=5434 dbname=replica_usecases host=localhost user=student password=student'
PUBLICATION revenue;

Page 16: Page 16

NOTICE:  created replication slot "spb" on publisher
CREATE SUBSCRIPTION

Work is in full swing at the branch offices:

β=> INSERT INTO revenue
  SELECT 'Moscow', round(random()*1_000_000)
  FROM generate_series(1,70);
INSERT 0 70
γ=> INSERT INTO revenue
  SELECT 'Saint Petersburg', round(random()*1_000_000)
  FROM generate_series(1,10);
INSERT 0 10

And the central office sees the activity of the entire company (let’s wait a few seconds for replication to catch up):

α=> SELECT city, sum(amount) FROM revenue GROUP BY city;
       city       |   sum
------------------+----------
 Saint Petersburg |  4633639
 Moscow           | 34529837
(2 rows)

Page 17: Server Upgrades

Source slide page 17: Server Upgrades{#source-figure-017 caption=“Source slide page 17: Server Upgrades”}

leader and physical replica: major version upgrade required

Speaker notes

Logical replication can be used to upgrade the server’s major version with zero or minimal downtime.

Let’s say we have a main server and a physical streaming replica.

Page 18: Server Upgrades

Source slide page 18: Server Upgrades{#source-figure-018 caption=“Source slide page 18: Server Upgrades”}

clients not connected yet

Speaker notes

We set up a new server running the required PostgreSQL version and migrate the table structure from the selected database to it.

On the primary server running the previous version, we publish changes for all database tables. Since schema changes are not replicated, they must be prohibited during the upgrade process.

Page 19: Server Upgrades

Source slide page 19: Server Upgrades{#source-figure-019 caption=“Source slide page 19: Server Upgrades”}

clients not connected yet

Speaker notes

We create a physical replica that mirrors the replica from the previous server version.

We end up with a parallel server setup running the new PostgreSQL version. This setup mirrors the one we have for the old version.

Page 20: Server Upgrades

Source slide page 20: Server Upgrades{#source-figure-020 caption=“Source slide page 20: Server Upgrades”}

clients connect to the new server

Speaker notes

After that, we switch clients over to the new servers and shut down the old ones. The service interruption will depend on how smoothly this switchover can be performed.

Keep in mind that this approach isn’t universally applicable: it comes with quite a few limitations and is significantly more complex than using the pg_upgrade utility, which can perform the upgrade in a short amount of time.

Page 21: Leader-Leader

Source slide page 21: Leader-Leader{#source-figure-021 caption=“Source slide page 21: Leader-Leader”}

a cluster where multiple servers can modify data

Speaker notes

Case: provide reliable data storage on multiple servers with the ability to write the data on any node (particularly useful for systems distributed across multiple locations).

This can be achieved through bidirectional logical replication, synchronizing changes for the same tables between servers in both directions (while limiting allowed replication origins, as was discussed in the “Logical Replication” module.

This requires that the applications working with the cluster are built with certain considerations in mind in order to avoid conflicts when modifying data in the same tables. For example, to use globally unique identifiers or to ensure that different servers work with different ranges of keys. You can also use the row filtering capabilities introduced in PostgreSQL 15.

Keep in mind that the leader-leader setup with logical replication will not support global distributed transactions by itself. With synchronous replication, reliability can be ensured, but consistency of data between servers cannot. In addition, PostgreSQL does not offer any tools for automatic failure processing, connecting or removing nodes from the cluster, etc. These tasks must be solved by external means.

Page 22: Decoding on Replica

Source slide page 22: Decoding on Replica{#source-figure-022 caption=“Source slide page 22: Decoding on Replica”}

logical slot can be created on a physical replica when creating a logical slot, you need a list of active transactions

Speaker notes

Starting with PostgreSQL 16, the logical publication slot can be created on a physical replica, which removes some of the load from the leader.

On the replica, enable hot_standby_feedback so that the leader does not delete the tuples needed by the replica prematurely.

When creating a logical replication slot, you need to know the active transaction IDs. This information is collected from the bgwriter and checkpointer processes on the leader as a WAL record. If there is no activity on the leader, slot creation may be delayed. To avoid waiting, you can call the pg_log_standby_snapshot function on the leader.

https://postgrespro.com/docs/postgresql/16/logicaldecoding-explanation

https://postgrespro.com/docs/postgresql/16/functions-admin

Page 23: Takeaways

Physical replication is a fundamental mechanism for addressing a wide range of challenges

  • ensuring high availability
  • load balancing for uniform workloads
  • separating different types of workloads
  • etc.
  • Logical replication extends these capabilities

Page 24: Practice

  1. Configure physical streaming replication between alpha and beta. 2. Configure cascade replication to gamma with a WAL record application delay of ten seconds. 3. Verify that replication is working and confirm that data appears on gamma with the configured delay. 4. Switch to beta after stopping the leader. 5. Confirm that gamma continues to operate as a replica.

Speaker notes

After completing the practice, remove the created database, the replication slot, and reset the configuration.

Page 25: 1. Configuring Replication Between Alpha and Beta

First, create a database.

student$ psql
α=> CREATE DATABASE replica_usecases;
CREATE DATABASE
α=> \c replica_usecases
You are now connected to database "replica_usecases" as user "student".

Create a replication slot and take a standalone backup.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ pg_basebackup \
  -p 5432 \
  --checkpoint=fast \
  --pgdata=/var/lib/postgresql/16/beta \
  -R \
  --create-slot \
  --slot=replica

The postgresql.auto.conf and standby.signal files were created by pg_basebackup.

postgres$ cat /var/lib/postgresql/16/beta/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
primary_conninfo = 'user=postgres passfile=''/var/lib/postgresql/.pgpass''
channel_binding=prefer port=5432 sslmode=prefer sslnegotiation=postgres sslcompression=0
sslcertmode=allow sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=prefer
krbsrvname=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable'
primary_slot_name = 'replica'
postgres$ ls -l /var/lib/postgresql/16/beta/standby.signal
-rw------- 1 postgres postgres 0 Sep 17 00:21 /var/lib/postgresql/16/beta/standby.signal

Start the replica:

student$ sudo pg_ctlcluster 16 beta start

2. Configuring Replication Between Beta and Gamma

student$ psql -p 5433 -d replica_usecases

Let’s create a standalone backup on beta to demonstrate that backups can be created from a replica. The slot is created by the utility.

student$ sudo pg_ctlcluster 16 gamma stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/gamma
postgres$ pg_basebackup \
  -p 5433 \
  --checkpoint=fast \
  --pgdata=/var/lib/postgresql/16/gamma \
  --create-slot --slot=replica \
  -R

The postgresql.auto.conf file was created by pg_basebackup. Now let’s add a replay delay:

postgres$ echo "recovery_min_apply_delay = '10s'" | tee -a
/var/lib/postgresql/16/gamma/postgresql.auto.conf
recovery_min_apply_delay = '10s'

Check the result:

Page 26: Page 26

postgres$ cat /var/lib/postgresql/16/gamma/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
primary_conninfo = 'user=postgres passfile=''/var/lib/postgresql/.pgpass''
channel_binding=prefer port=5432 sslmode=prefer sslnegotiation=postgres sslcompression=0
sslcertmode=allow sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=prefer
krbsrvname=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable'
primary_slot_name = 'replica'
primary_conninfo = 'user=postgres passfile=''/var/lib/postgresql/.pgpass''
channel_binding=prefer port=5433 sslmode=prefer sslnegotiation=postgres sslcompression=0
sslcertmode=allow sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=prefer
krbsrvname=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable'
primary_slot_name = 'replica'
recovery_min_apply_delay = '10s'

Start the gamma server.

student$ sudo pg_ctlcluster 16 gamma start

3. Testing the Setup

student$ psql -p 5434 -d replica_usecases

On alpha, create a table and verify that it appears on beta first, and then on gamma 10 seconds later.

α=> CREATE TABLE test(s text);
CREATE TABLE
α=> INSERT INTO test VALUES ('Hello, world!');
INSERT 0 1
β=> SELECT * FROM test;
       s
---------------
 Hello, world!
(1 row)

Check gamma:

γ=> SELECT * FROM test;
ERROR:  relation "test" does not exist
LINE 1: SELECT * FROM test;
                      ^

The table isn’t there yet. Wait 10 seconds…

γ=> SELECT * FROM test;
       s
---------------
 Hello, world!
(1 row)

The table has appeared.

4. Promoting beta

Current timeline on gamma:

γ=> SELECT received_tli FROM pg_stat_wal_receiver;
 received_tli
--------------
            1
(1 row)

When beta is promoted, the timeline number will increment by one, and gamma’s recovery process will follow the new timeline due to the default value:

γ=> SELECT setting, boot_val FROM pg_settings WHERE name = 'recovery_target_timeline';

Page 27: Page 27

Source slide page 27: Page 27{#source-figure-027 caption=“Source slide page 27: Page 27”}

 setting | boot_val
---------+----------
 latest  | latest
(1 row)
student$ sudo pg_ctlcluster 16 alpha stop
student$ sudo pg_ctlcluster 16 beta promote

The timeline on gamma has changed.

γ=> SELECT received_tli FROM pg_stat_wal_receiver;
 received_tli
--------------
            2
(1 row)

5. Testing the Setup

On beta, add a row to the table and verify that it appears on gamma 10 seconds later.

β=> INSERT INTO test VALUES ('After switching to the second server');
INSERT 0 1

Test it:

γ=> SELECT * FROM test;
       s
---------------
 Hello, world!
(1 row)

The second row isn’t there yet. Wait 10 seconds…

γ=> SELECT * FROM test;
                  s
--------------------------------------
 Hello, world!
 After switching to the second server
(2 rows)

The row has appeared.

Cleanup

Delete the created databases and replication slots, reset the configuration.

γ=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
γ=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 gamma restart
β=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
β=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
β=> DROP DATABASE replica_usecases;
DROP DATABASE
β=> SELECT pg_drop_replication_slot('replica');

Page 28: Page 28

 pg_drop_replication_slot
--------------------------
(1 row)
student$ sudo pg_ctlcluster 16 beta stop
student$ sudo pg_ctlcluster 16 gamma stop
student$ sudo pg_ctlcluster 16 alpha start
student$ psql
α=> SELECT pg_drop_replication_slot('replica');
 pg_drop_replication_slot
--------------------------
(1 row)
α=> DROP DATABASE replica_usecases;
DROP DATABASE

Page 29: Practice+

1. Configure physical replication between alpha and beta with feedback. Note the WAL level required to configure logical replication, you will need it later. 2. Configure logical replication of a table from alpha to gamma by creating a replication slot on beta – a physical replica. To speed up slot creation on beta, use the pg\_log\_standby\_snapshot function.

Speaker notes

  1. Transfer the structure of the published table to gamma, for example, using pg_dump. Do not transfer any data.

The publication must be created on alpha, subscription on gamma, and beta set in the subscription connection string.

Reset the system: delete the new database and the replication slot and reset the configuration.

Page 30: 1. Physical Replication from Alpha to Beta

Enable feedback right away and set the WAL level required for logical replication:

α=> ALTER SYSTEM SET hot_standby_feedback TO on;
ALTER SYSTEM SET wal_level TO logical;
ALTER SYSTEM
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

Create a database and a table.

student$ psql
α=> CREATE DATABASE replica_usecases;
CREATE DATABASE
α=> \c replica_usecases
You are now connected to database "replica_usecases" as user "student".
α=> CREATE TABLE test(
  id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
  descr text
);
CREATE TABLE

Create a standalone backup.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ pg_basebackup \
  -p 5432 \
  --checkpoint=fast \
  --pgdata=/var/lib/postgresql/16/beta \
  -R \
  --create-slot --slot=replica
student$ sudo pg_ctlcluster 16 beta start

Verify that physical replication is working.

α=> INSERT INTO test(descr) VALUES('Physical replication');
INSERT 0 1
student$ psql -p 5433 -d replica_usecases
β=> SELECT * FROM test;
 id |        descr
----+----------------------
  1 | Physical replication
(1 row)

2. Logical Replication from Beta to Gamma

Copy the database and table structure from beta to gamma using pg_dump.

student$ sudo pg_ctlcluster 16 gamma start
student$ pg_dump --create --schema-only --no-acl -d replica_usecases -p 5433 | psql -p 5434
SET
SET
SET
SET
SET
 set_config
------------

Page 31: Page 31

(1 row)
SET
SET
SET
SET
CREATE DATABASE
ALTER DATABASE
You are now connected to database "replica_usecases" as user "student".
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
SET
CREATE TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
student$ psql -p 5434 -d replica_usecases
γ=> SELECT * FROM test;
 id | descr
----+-------
(0 rows)

We copied only the table structure, not the data. Create a publication.

α=> CREATE PUBLICATION test_pub FOR TABLE test;
CREATE PUBLICATION
β=> \dRp+
                           Publication test_pub
  Owner  | All tables | Inserts | Updates | Deletes | Truncates | Via root
---------+------------+---------+---------+---------+-----------+----------
 student | f          | t       | t       | t       | t         | f
Tables:
    "public.test"

Subscribe to the changes in the test table published by the replica (beta).

γ=> CREATE SUBSCRIPTION test_sub
CONNECTION 'user=student password=student dbname=replica_usecases host=localhost port=5433'
PUBLICATION test_pub;

The subscription is waiting for a slot to be created on the beta server. To speed things up, let’s ask alpha to write a record with active transaction IDs to the WAL:

α=> SELECT pg_log_standby_snapshot();
 pg_log_standby_snapshot
-------------------------
 0/305AD80
(1 row)
NOTICE:  created replication slot "test_sub" on publisher
CREATE SUBSCRIPTION
γ=> \dRs test_sub

Page 32: Page 32

Source slide page 32: Page 32{#source-figure-032 caption=“Source slide page 32: Page 32”}

           List of subscriptions
   Name   |  Owner  | Enabled | Publication
----------+---------+---------+-------------
 test_sub | student | t       | {test_pub}
(1 row)

The slot and subscription have been created. Wait a moment… The table row has appeared on the logical replica.

γ=> SELECT * FROM test;
 id |        descr
----+----------------------
  1 | Physical replication
(1 row)

Let’s add one more row.

α=> INSERT INTO test(descr) VALUES('Logical replication');
INSERT 0 1
γ=> SELECT * FROM test;
 id |        descr
----+----------------------
  1 | Physical replication
  2 | Logical replication
(2 rows)

Cleanup

Delete the subscription, the database, and the replication slot, then reset configuration.

γ=> DROP SUBSCRIPTION test_sub;
NOTICE:  dropped replication slot "test_sub" on publisher
DROP SUBSCRIPTION
γ=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
γ=> DROP DATABASE replica_usecases;
DROP DATABASE
γ=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 gamma stop
α=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
α=> DROP DATABASE replica_usecases;
DROP DATABASE
β=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
β=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta stop
postgres$ rm -f /var/lib/postgresql/16/beta/standby.signal
α=> SELECT pg_drop_replication_slot('replica');
 pg_drop_replication_slot
--------------------------
(1 row)

Page 33: Page 33

α=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
α=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

08 Lesson 08: Clustering Technologies / Overview

Native Markdown source page: Clustering Technologies / Overview
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: No matching course-page link was captured in the source inventory. Archive: DBA3-handouts-16-en-20260917.zip PDF pages: 28

Page 1: Clustering Technologies / Overview

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Alexey Beresnev Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Performance Expectations
  • Enablers
  • Building Blocks
  • System Examples

Page 3: Performance Expectations

High system availability

  • and reliability
  • Scalability
  • Data consistency

Page 4: High Availability

Minimizing system downtime

  • caused by both failures and scheduled maintenance
  • Some metrics
    • system uptime ratio
    • → 100 %
    • MTBF – Mean Time Between Failures
    • → ∞
    • RTO – Recovery Time Objective
    • → 0
    • RPO – Recovery Point Objective
    • → 0
    • and others

Speaker notes

System availability is the ratio of time the system remains accessible to its total operational time.

Availability is a broader concept than reliability, as downtime can result not only from failures but also from scheduled maintenance.

Availability is often expressed in percentages:

● “one nine” 90% – 36.5 days of downtime per year,

● “two nines” 99% – 3.65 days of downtime per year,

● “three nines” 99.9% – 8.76 hours of downtime per year,

● “four nines” 99.99% – 52.56 minutes of downtime per year,

● “five nines” 99.999% – 5.26 minutes of downtime per year, and so on.

High availability means getting close to 100%.

For example, managed PostgreSQL database service in Yandex Cloud guarantees 99.99% availability for read nodes and 99.95% for write nodes. (https://yandex.cloud/en/services/managed-postgresql).

Some other availability metrics:

● Mean Time Between Failures (MTBF);

● Recovery Time Objective (RTO): the time it takes for the system to recover;

● Recovery Point Objective (RPO) — the maximum amount of data that can be lost during a failure, and so on.

Page 5: Scalability

Scalability

  • The system’s ability to handle higher load with proportionally more resources
  • provided
  • ideal response time scalability —
  • response time should remain constant
  • ideal throughput scalability —
  • the number of processed requests should increase linearly
  • Speedup
    • the ratio of response time on a single node to response time in a cluster with
    • N nodes

Speaker notes

A key concept for clusters is scalability: it describes how various target metrics react as the number of nodes, data volume, and workload handled by the system increase. The workload increases both as a result of more queries processed in the same amount of time and because of higher volumes of data overall. Resources in this context usually refer to the number of nodes.

Response time scalability reflects how response time (the time it takes to process a query and respond with a result) changes with increased load and more cluster nodes. This is paramount for OLTP systems. Ideally, response time should remain constant at scale.

Speedup is a derivative metric that demonstrates how response time changes with increased number of nodes, but at a constant load.

Throughput scalability measures how the number of processed requests per second changes as data volume and the number of nodes grow. This is the most important metric for OLAP systems. Ideally, throughput should scale linearly with the number of nodes added.

Perfect scalability (unfortunately unattainable in practice) would let you handle growth simply by adding more nodes to the cluster.

Page 6: Consistency

Local transactions

  • virtually no guarantees when switching between nodes
  • applications must handle consistency concerns
  • Global (distributed) transactions
    • ACID consistency
    • transaction atomicity is necessary

Speaker notes

The stronger the data consistency guarantees a cluster provides, the more it behaves like a single unified system, making it easier to work with and requiring less special handling in application code. However, this comes at a cost of performance.

In a master-replica cluster with a single write node where transactions execute locally, consistency guarantees can be quite weak. For instance, one node may have already applied the transaction commit and made its results available to clients, while another node has not yet done so. In this scenario, a client may end up reading stale data from one node after having already read the updated value from another node. Applications must be designed with these situations in mind and be able to handle this configuration.

For a cluster providing global (distributed) transactions, there is consistency in the traditional ACID sense: a global transaction must move the database from one consistent state to another (C), provided the transaction completes fully across all nodes (A) and is isolated from other concurrent transactions (I).

This requires ensuring transaction atomicity: all cluster nodes must take the same action — either commit or roll back the transaction. PostgreSQL offers a relatively straightforward two-phase commit protocol (2PC) for this purpose, though it is not entirely immune to faults.

Page 7: Enablers

High Availability

  • Data Synchronization between Nodes
  • Failure Types and Detection
  • Distributed Consensus Protocols
  • Scalability

Page 8: High Availability

Scheduled maintenance without service interruption

  • using PostgreSQL stock tools
  • temporary node removal from the cluster
  • Redundant components to eliminate single points of failure
    • PostgreSQL servers (and other required systems)
    • network hardware
    • power supply
  • Failure detection and recovery management

Speaker notes

Downtime can result not only from unexpected failures but also from scheduled maintenance that cannot be performed without interrupting service. Each new PostgreSQL release reduces the number of operations that require a server restart. Additionally, the cluster must be able to handle planned shutdown of a node with minimal impact on users.

One of the pillars of fault tolerance is redundancy across all system components: not only database servers but also network equipment, power supply systems, and other infrastructure.

But simply duplicating system components isn’t enough. First, you have to set up data synchronization between nodes. Then, you need failure detection and handling – proper failover to standby nodes and recovery after resolving the failures.

Vanilla PostgreSQL provides limited, if any, native support for setting these up. Generally speaking, the higher the system’s target availability, the more complex its architecture.

Page 9: Synchronization

Shared nothing

  • physical replication
  • logical replication
  • foreign tables, postgres_fdw
  • Shared disk
    • no synchronization, but needs a fault-tolerant storage
    • no stock shared disk support

Speaker notes

If you want a cluster of vanilla PostgreSQL servers, your only bet is the shared nothing architecture: each server has its own storage and memory, and they talk to each other over the network. Data is synchronized by means of replication, which we explored previously.

Physical replication, unidirectional as it is, lends itself to setups with one leader and multiple replicas.

Logical replication, the more flexible of the two, supports bidirectional synchronization setups as well as selective replication, down to specific tables and rows. It enables geographically distributed clusters with multiple servers running independently, but sharing any changes.

Selective replication can also be achieved by other means, such as external tables with the postgres_fdw utility.

The shared disk architecture requires modifications to the PostgreSQL core, but offers some exciting new opportunities. With a shared disk, synchronization is unnecessary (the storage itself might use block-level replication for redundancy, but it appears as a single device to PostgreSQL). Since compute nodes are stateless, they can be started and stopped at any time. This approach is a natural fit for dynamic workloads and is ideal for cloud environments.

Page 10: Node Failure

Source slide page 10: Node Failure{#source-figure-010 caption=“Source slide page 10: Node Failure”}

failure detection: periodic signal indicating normal operation (heartbeat)

Speaker notes

Before discussing failure detection and management, we need to define what constitutes a failure. From a cluster software perspective, there are typically two types of failures: node failure and network partitioning.

When a node fails, it stops operating as part of the cluster. A failure can result from a database process stop or crash, an operating system crash, server shutdown, or similar issues.

To detect failures, cluster nodes periodically exchange brief heartbeat messages. When a node fails to respond in time, the system registers the failure and initiates the appropriate recovery procedures.

The failure recovery procedure depends on the cluster architecture. In a leader-replica cluster, for example, if the leader fails, one of the replicas must take over its role through a failover.

Page 11: Network Partition

Source slide page 11: Network Partition{#source-figure-011 caption=“Source slide page 11: Network Partition”}

split-brain: the cluster fragments into independent, concurrently operating segments that each accept write requests

  • node failure, network failure and lag all look identical from the outside

Speaker notes

A network partition occurs when network equipment fails. When this happens, all nodes may remain operational, but one group of nodes loses connectivity with the other.

The worst case is when both groups maintain connectivity with clients. This can lead to a split-brain: the cluster splits into two segments, each operating independently and continuing to accept write requests. This leads to data divergence, and when recovery takes place, one group’s data will inevitably be lost.

Note that a network can fail completely, or it may experience temporary latency or packet loss. (Additionally, in some operating systems, factors such as CPU load can cause network delays). From the cluster’s perspective, all these scenarios are indistinguishable from a node failure: if a node doesn’t send an heartbeat in time, the cluster must consider it downed. A node may or may not still be functioning, but there’s no way to tell without any connection to it.

Many systems will operate unreliably when network connectivity is unstable. To reduce the likelihood of false positives, you can increase the threshold value, but this will also increase the time it takes to detect an actual failure, which in turn reduces availability. For example, a popular distributed key-value store etcd suggests setting the threshold to ten times the regular node response time.

https://github.com/etcd-io/etcd

Page 12: The Consensus Problem

Cluster nodes must be able to reach consensus

  • each node sees the same current state of the whole cluster
  • atomic commits of global transactions
  • Consensus through messages
    • no shared memory (shared nothing)
    • dedicated distributed protocols that account for potential failures at every
    • stage of voting
    • Designing, proving correctness, and testing the implementation are all
    • challenging tasks!

Speaker notes

A cluster made up of independent nodes must operate as a single system, in a sense. To handle failures, nodes must share a common understanding of each other’s states. To commit a global transaction, all cluster nodes must agree that it does not break anything, and then execute the commit atomically.

Since the nodes have no shared storage (we’re looking at shared-nothing systems here), the only way to reach agreement is through a distributed consensus protocol. The protocol specifies what messages nodes need to exchange – and in what order – to reach consensus.

Distributed algorithms are extremely complex because they must account for numerous edge cases. For example, they must function correctly even when a failure occurs at any stage of the voting process. Proving the correctness of such algorithms and testing their implementations is equally challenging. You shouldn’t attempt to create your own or trust unproven ones.

Page 13: 2PC and 3PC Protocols

Two-phase commit

  • prepare:
  • the coordinator holds a vote
  • commit:
  • if every node votes yes, the coordinator signals to commit
  • supported by PostgreSQL (PREPARE TRANSACTION + COMMIT)
  • Three-phase commit
    • prepare:
    • the coordinator holds a vote
    • precommit: if every node votes yes, the coordinator notifies all participants
    • of the intent to commit
    • commit:
    • if all nodes acknowledge, the coordinator signals to commit
  • Properties
    • there are situations (such as network partition) where a transaction cannot be
    • resolved until the failure is fixed

Speaker notes

PostgreSQL comes with a straightforward two-phase commit (2PC) protocol that handles global commits. The node executing the transaction acts as the coordinator. During the prepare phase, the coordinator informs the other nodes about the transaction commit and waits for them to confirm that that they’re ready to commit (if at least one node isn’t ready, the transaction is aborted). During the commit phase, the coordinator orders all nodes to commit the transaction. Once the coordinator receives commit confirmation from all nodes, it commits the transaction as well.

This protocol requires minimal message exchange but isn’t fault-tolerant. If the coordinator fails after the prepare phase, the remaining nodes have no way of knowing whether to commit or abort the transaction. They’ll have to wait until the failure is resolved.

A more sophisticated protocol called three-phase commit (3PC) addresses this limitation. It introduces an additional precommit phase where the coordinator informs all nodes of its *decision* to commit the transaction. After that, if the coordinator fails, the remaining nodes can proceed to commit the transaction on their own (or abort it independently if the failure occurs before the precommit phase).

However, both algorithms break down in network partition scenarios.

Page 14: Consensus protocols

Source slide page 14: Consensus protocols{#source-figure-014 caption=“Source slide page 14: Consensus protocols”}

Paxos, Raft, ZAB, E3PC, and others

  • if up to N/2–1 nodes fail, the connected majority continues operating
  • the minority remains blocked until the failure is resolved

Speaker notes

Consensus (majority voting) algorithms enable nodes that maintain connectivity with each other and form a quorum to continue operating during network partitions. With equal voting weight across all nodes, N/2+1 out of N nodes can remain operational.

Nodes in the minority must wait for the failure to be resolved before they can commit or roll back their open transactions. These nodes can either stop serving clients entirely or at least refuse write requests to prevent split-brain.

Several mathematically proven algorithms exist with battle-tested implementations.

● Paxos was published in 1998, though it actually originated in the late 1980s. There’s an entire family of these algorithms designed for different scenarios.

● Raft was designed as an algorithm equivalent to Paxos but easier to comprehend. In Raft, one node is elected as the leader with write privileges, while the remaining nodes are read-only. It became the basis for distributed key-value storage systems like etcd and Consul.

● ZAB is the algorithm used by ZooKeeper.

● Enhanced 3PC (E3PC) improves upon the three-phase commit algorithm by allowing transaction commits with a majority vote rather than requiring unanimous agreement.

Other approaches exist for solving the consensus problem, such as Virtual Synchrony, which is used by the Corosync system.

Page 15: Scalability

Read throughput

  • load distribution across cluster nodes
  • Write throughput
    • cannot be achieved with full data replication to all nodes
    • sharding (distributing table rows across different nodes) depends heavily on
    • the nature of the data being stored
  • Response time
    • parallel computation across multiple nodes

Speaker notes

Read throughput is the easiest to scale: just distribute read transactions more or less evenly across the cluster nodes.

Scaling write throughput is typically more challenging: in leader-leader configurations, nodes must exchange messages and data with each other, which means the overall system load actually increases as more nodes are added. However, this becomes feasible when data is distributed across nodes rather than being fully replicated to each one.

Sharding – distributon of data at table row level – is one approach that is used often. You can think of it as partitioning where the partitions are located on different cluster nodes.

Note that specific sharding schemes are heavily dependent on the data logic; there’s no one-size-fits-all solution here.

Sharding also enables response time scalability. To achieve this, the cluster needs to be able to parallelize queries by executing them simultaneously across multiple nodes that hold the required data. This is similar to parallel query execution in standard PostgreSQL, except the parallel processes run across different nodes instead of within a single server. Of course, this increases the overhead of data transfer between processes, such as when passing partial aggregation results.

Page 16: Load Balancing

PostgreSQL-specific tools

  • libpq library
  • PgBouncer – connection pool manager
  • Pgpool-II – load balancer and connection pool manager
  • Odyssey – multithreaded connection pool manager
  • …
  • Other tools
    • virtual IP address
    • HAProxy – TCP/HTTP load balancer
    • round-robin DNS
    • …

Speaker notes

There are plenty of ready-made solutions available for load distribution; typically, these tools are not part of the database cluster itself.

● The libpq connection string supports multiple hosts. The order of connections is defined by the load_balance_hosts parameter, and target_session_attrs lets you set session attributes, such as read-only connections.

● PgBouncer is a lightweight connection pooling manager for PostgreSQL.

● Pgpool-II is a connection pooling manager and load balancer that can route write traffic to the leader and read traffic to replicas.

● Odyssey is a multithreaded connection pooling manager with full SSL/TLS support and LDAP and PAM authentication capabilities.

Some general-purpose tools not specific to PostgreSQL:

● Virtual IP address – an IP controlled by the cluster that points the clients to specific nodes.

● HAProxy is a load balancer and proxy server that operates at the TCP and HTTP layers. Node lists can be kept up to date with tools like confd, or you can use HAProxy’s own health check tools to keep track of healthy nodes and kick faulty ones from the balancing pool.

● Some systems (like Consul) provide round-robin DNS: node selection and load balancing can happen at the name resolution stage (though caching negates the benefits of this approach).

There are other tools available as well, and many of them work well together.

Page 17: Components and examples

Required Third-Party Components

  • System Examples

Page 18: Reliability

Agent

  • manages a single PostgreSQL instance
  • Control node
    • cluster-wide management logic
  • Consensus
    • external system managed by a consensus protocol
    • shared state support for control nodes
  • Entry point
    • a mechanism to route clients to the appropriate cluster node

Speaker notes

If what you really want from your cluster above else is high availability and reliability, the obvious route is setting up several instances of baseline PostgreSQL and linking them together with some other software. Data synchronization, then, will be achieved by physical replication, meaning that the cluster will have one leader and several replicas.

Since PostgreSQL cannot configure, restart, or stop itself, a dedicated program is needed to handle these operations. This role is performed by an agent.

The cluster management logic is handled by control nodes. At this level, decisions are made about which nodes should be considered failed, which replica should be promoted to the new leader when the old one fails, and so on.

To maintain a shared view of the cluster state, the control nodes rely on a system that implements one of the distributed consensus protocols discussed earlier.

Finally, clients need a single entry point that routes them to the appropriate cluster node or nodes.

Different clustering systems may use different names for these components, and some may be combined into a single entity. If certain components are missing, the cluster must be supplemented with third-party solutions.

Page 19: Fencing

Isolating a node in an unknown state from clients

  • a network partition can lead to split-brain
  • must not only isolate the node but also prevent failover to a replica during
  • this time
  • Mechanisms
    • power cutoff
    • network port cutoff
    • client connection termination and redirection

Speaker notes

Since PostgreSQL servers in this approach are unaware they’re operating within a cluster, it’s essential to be able to fence off a failed node (which may still be running but has lost connectivity to the cluster) from the outside world. This procedure is called fencing.

Without proper fencing, a leader that ends up in the minority partition during a network split could continue operating, leading to split-brain. You cannot rely on the control nodes to stop the PostgreSQL server on their own when they detect a failure: they might lose connection with the agent, the agent itself could fail, and so on.

The procedure cannot depend on the node being reachable over the network. Cutoff must be set up differently: For example:

● programmatically power off the server (using IPMI or PDU interfaces);

● programmatically disable the network port on the switch;

● if connections go through a specialized component, clients can be forcibly disconnected from the failed node at that component’s level.

While fencing reduces the scope of the problem, it doesn’t completely solve it on its own. You must also ensure that failover to a replica cannot occur until the failed leader has been fenced. After all, making the fencing decision takes time, during which the node may continue operating, meaning some committed data could be lost during recovery (zero RPO cannot be achieved).

Page 20: Example: Patroni

Source slide page 20: Example: Patroni{#source-figure-020 caption=“Source slide page 20: Example: Patroni”}

Speaker notes

Patroni (https://github.com/patroni/patroni ) is an open-source clustering solution designed for use in both cloud and dedicated server environments.

Here, the agent and control functions are combined in a single component. Consensus is achieved using a key-value store provided by etcd (Raft), Consul (Raft), or ZooKeeper (ZAB).

A separate solution is needed to provide the entry point. The typical recommendation is to use HAProxy together with confd, which periodically updates the HAProxy configuration based on information from the consensus layer. The standard configuration routes all clients to the node serving as the leader.

  • To prevent HAProxy from becoming a single point of failure, you need
  • multiple HAProxy servers and a virtual IP address for client connections.
  • However, this means that configurations across different HAProxy servers
  • must be updated synchronously, bringing us back to the clustering problem
  • just at a different level.

Node fencing can be implemented using the Linux kernel’s watchdog timer mechanism. If the node, while serving as the leader, cannot contact the consensus layer for a certain period, the timer triggers an operating system reboot.

Page 21: Example: The ClusterLabs Stack

Source slide page 21: Example: The ClusterLabs Stack{#source-figure-021 caption=“Source slide page 21: Example: The ClusterLabs Stack”}

Speaker notes

ClusterLabs (https://clusterlabs.org/) provides an open source component stack for building clusters from virtually any systems.

The agent (referred to as a resource agent in this framework) treats each specific system as a standardized resource by implementing operations like stop, start, replica promotion, and so on. This component needs to be implemented separately for each system.

Pacemaker, the controller component, interacts with resources through their agents. Corosync (Virtual Synchrony) handles cluster-wide state management and failure detection.

This architecture focuses heavily on node fencing. This approach uses the STONITH technology (Shoot The Other Node In The Head), which reliably isolates a failed node from both the cluster and clients by remotely cutting power or disabling its switch port.

The client entry point is typically a virtual IP address that the controller node assigns to whichever node is currently serving as the leader.

If needed, you can also distribute read workload across replicas using tools like HAProxy, PgBouncer, and similar solutions.

For PostgreSQL specifically, there’s an open-source solution PAF (PostgreSQL Automatic Failover): http://clusterlabs.github.io/PAF/

Page 22: Example: BiHA

Source slide page 22: Example: BiHA{#source-figure-022 caption=“Source slide page 22: Example: BiHA”}

Speaker notes

A large number of external components clearly complicates cluster deployment and maintenance. To simplify these tasks, all necessary components can be built directly into PostgreSQL itself. This complex challenge has been addressed in the BiHA (Built-in High Availability) cluster, which is an integral part of Postgres Pro Enterprise.

From the outside, this cluster appears as regular nodes connected by physical replication, but in reality, each Postgres Pro Enterprise node implements failure detection, split-brain prevention, a consensus layer (using the Raft protocol), management logic, and an agent.

Consensus requires at least three nodes, though it’s not necessary to store data on all of them. You can deploy a cluster where the third node serves as a referee: it participates in the quorum but doesn’t take part in replication. A less powerful server can be used for this type of node.

No additional software is needed for the entry point: the cluster relies on the client’s ability to determine on its own whether it’s connected to the leader (which handles both read and write operations) or to a replica. Such capabilities are available in libpq, among other tools.

https://postgrespro.com/docs/enterprise/16/biha-solution

Page 23: Consistency

Global Transaction Manager

  • requires modifications to the PostgreSQL core
  • can be built in or implemented as a separate component

Speaker notes

In the previous examples, we added components to PostgreSQL that enable fault tolerance: agents, control nodes (including failure detection systems), and a consensus layer for controlling the state of the cluster. These components could exist either outside the PostgreSQL nodes or be integrated directly into PostgreSQL itself.

To create a cluster that provides clients with a unified, consistent view of data, you need a Global Transaction Manager that implements a distributed consensus protocol to ensure atomic commits for transactions spanning multiple nodes.

The ability to work with global transactions cannot be achieved through external means alone, it must be built into PostgreSQL by modifying the core. Even when global transaction management is implemented as a separate cluster component, PostgreSQL must still be aware that it needs to communicate with this component.

Typically, fault tolerance is required in addition to consistency. The cluster must then include all the components we discussed earlier. If the Global Transaction Manager is implemented as a separate component, you’ll also need to ensure its fault tolerance, which adds another layer of complexity to the system.

Page 24: Example: multimaster

Source slide page 24: Example: multimaster{#source-figure-024 caption=“Source slide page 24: Example: multimaster”}

Speaker notes

The Postgres Pro Enterprise multimaster extension is an example of such a system. It’s a Global Transaction Manager integrated into the database system that extends the existing two-phase commit protocol (2PC) to E3PC. To accomplish this, arbiter processes are launched on the nodes to handle additional communication. The system uses the Paxos consensus algorithm.

https://postgrespro.com/docs/enterprise/16/multimaster

Any node in the multimaster cluster (except the referee node, if one is configured) can accept both read and write queries. Read-only transactions are executed locally. Write transactions are distributed; the node that commits the transaction acts as the coordinator.

Data replication between cluster nodes relies on logical replication, while DDL changes are replicated using a proprietary algorithm.

To distribute load effectively, it makes sense to balance connections across cluster nodes. For example, you can assign different connection strings to different clients, or use HAProxy for load balancing.

An application running on a single server can be deployed on a multimaster cluster without any additional concerns about data consistency. The supported isolation levels are read committed and repeatable read. However, due to current implementation limitations, applications need to account for certain nuances.

Beyond implementing distributed transactions, the cluster also provides fault tolerance.

Page 25: Scalability

Large number of nodes

  • shared storage or replication
  • sharding
  • parallel processing

Speaker notes

When scalability becomes the primary concern, you can set up a relatively straightforward cluster built from standard PostgreSQL instances without the typical clustering infrastructure. This type of cluster enables load distribution across different nodes, though it won’t provide fault tolerance or consistency guarantees.

Data can be fully replicated across nodes (enabling read scalability) or partially replicated (for example, through sharding—which also enables write scalability). Systems designed for cloud infrastructure with dynamic resource scaling can use a shared storage approach where nodes are separated into compute and storage tiers.

Distributing data across a large number of nodes also enables parallel query execution.

Page 26: Example: Citus

Source slide page 26: Example: Citus{#source-figure-026 caption=“Source slide page 26: Example: Citus”}

Speaker notes

Citus (https://github.com/citusdata/citus ) is a PostgreSQL extension that distributes data and queries across cluster nodes.

Citus is an example of a system where data isn’t fully replicated but instead distributed across worker nodes through sharding. One node is designated as the coordinator and contains metadata about data location.

The extension includes a specialized query optimizer that generates parallel execution plans and an executor that runs queries by distributing them across the appropriate nodes. This approach enables a Citus cluster to scale both in terms of response time and write throughput.

Citus ensures atomic changes across all cluster nodes using global transactions with the two-phase commit (2PC) protocol.

Data rebalancing across worker nodes (when adding new nodes or removing old ones) relies on logical replication.

In this configuration, the cluster doesn’t provide high availability: if any node fails, some data will be lost. To address this, both the coordinator and worker nodes can be replaced with individual mini-clusters of three nodes each (one primary and two replicas) to ensure fault tolerance.

Page 27: Example: Neon

Source slide page 27: Example: Neon{#source-figure-027 caption=“Source slide page 27: Example: Neon”}

Speaker notes

Neon is an open-source solution for cloud-based serverless computing. The concept involves splitting the database management system into separate stateless nodes while leveraging large, inexpensive, yet slower data storage.

Compute nodes (essentially PostgreSQL instances stripped of their logging and disk operations) are launched within Kubernetes-managed containers. Since these nodes are stateless, containers can be spun up and shut down at any time based on client activity, eliminating costs for unused resources.

To persist data, the write instance (only one is allowed) sends WAL records to the safekeeper component. The records from committed transactions must be stored reliably; to achieve this, safekeeper components form a separate fault-tolerant cluster that operates using the Paxos protocol.

WAL records are asynchronously forwarded from the safekeeper to the pageserver component, which applies them to the data pages. The pageserver retrieves pages on demand from clients, flushing them to bottomless S3 object storage and reading them back as needed. In this way, the pageserver acts as a cache for actively used data, functioning as the equivalent of a buffer cache.

Neon scales for read operations and automatically adjusts to workload demands, though it inevitably performs slower than traditional systems.

https://github.com/neondatabase/neon

Page 28: Takeaways

Building a cluster requires tools that aren’t included in standard PostgreSQL

  • There are numerous approaches to building a cluster
    • different objectives: high availability, scalability, and consistency
    • solutions involving external components or kernel modifications
    • solutions with global transaction support
  • The higher the requirements, the more complex the system becomes and the higher the cost