A full backup provides a baseline without depending on an earlier backup from the same method. An incremental backup stores changes relative to a specified reference and depends on the material needed to reconstruct the omitted content. That distinction describes recovery dependencies, not a universal promise about speed or bytes transferred. A large changed working set can make an incremental backup substantial too.

When it breaks

A damaged full backup can invalidate every incremental that depends on it. Likewise, removing a required middle increment can break later recovery even when the newest directory and its manifest remain intact. Retention must follow dependencies rather than independently deleting directories whose timestamps look old. A newer file is not automatically a replacement for all older recovery material.

The reference matters. A backup relative to the previous increment forms a chain; one relative to a full baseline has a different dependency shape, often described as differential. A synthetic full reconstructs a new baseline from existing backup material. These terms do not guarantee identical implementations across tools. Check the exact product’s manifest and restore rules before designing rotation around the names.

Physical database backups also need the WAL required to make their captured files consistent. A full backup is independent of earlier backups, not independent of all recovery logs, software, configuration, or external dependencies. The experiment includes the required WAL through streaming and keeps the database version and checksum configuration consistent across the chain.

Check it quickly

Run the following as a non-root Linux account with PostgreSQL 18 server and client binaries, including pg_combinebackup, in PATH. It was executed on PostgreSQL 18.6. The script creates private temporary primary and recovery clusters with no TCP listeners. WAL summarization is enabled before taking backups so PostgreSQL can identify changed blocks for the incremental requests.

#!/bin/sh
set -eu
umask 077
lab=$(mktemp -d /tmp/xbcatm-incremental.XXXXXX)
mkdir "$lab/socket"
primary=$lab/primary
restored=$lab/restored
cleanup() {
    for cluster in "$restored" "$primary"; do
        if test -f "$cluster/postmaster.pid"; then
            pg_ctl -D "$cluster" -m fast -w stop >/dev/null
        fi
    done
}
trap cleanup EXIT
initdb -D "$primary" --no-locale --encoding=UTF8 --auth-local=trust --auth-host=reject >/dev/null
cat >> "$primary/postgresql.conf" <<CONF
listen_addresses = ''
unix_socket_directories = '$lab/socket'
port = 56432
summarize_wal = on
CONF
pg_ctl -D "$primary" -l "$lab/primary.log" -w -t 15 start >/dev/null
export PGHOST=$lab/socket PGPORT=56432 PGDATABASE=postgres PGUSER=$(id -un)
export PGCONNECT_TIMEOUT=2
psql -X -v ON_ERROR_STOP=1 -qAt <<'SQL'
CREATE TABLE marker (id integer PRIMARY KEY,value text);
INSERT INTO marker VALUES (1,'full');
SQL
pg_basebackup -D "$lab/base" -X stream -c fast -w
psql -X -v ON_ERROR_STOP=1 -qAt -c "INSERT INTO marker VALUES (2,'first_increment')"
pg_basebackup -D "$lab/inc1" --incremental="$lab/base/backup_manifest" -X stream -c fast -w
psql -X -v ON_ERROR_STOP=1 -qAt <<'SQL'
UPDATE marker SET value='final' WHERE id=1;
DELETE FROM marker WHERE id=2;
INSERT INTO marker VALUES (3,'second_increment');
SQL
pg_basebackup -D "$lab/inc2" --incremental="$lab/inc1/backup_manifest" -X stream -c fast -w
for backup in "$lab/base" "$lab/inc1" "$lab/inc2"; do
    pg_verifybackup -q "$backup"
done
echo input_backups_verified
if pg_combinebackup -o "$lab/missing-middle" "$lab/base" "$lab/inc2" >"$lab/missing-middle.log" 2>&1; then
    echo 'incomplete chain unexpectedly combined' >&2
    exit 1
fi
grep -E 'starts at LSN .*but expected' "$lab/missing-middle.log"
echo missing_middle_rejected
pg_combinebackup -o "$lab/combined" "$lab/base" "$lab/inc1" "$lab/inc2"
pg_verifybackup -q "$lab/combined"
echo combined_backup_verified
mkdir "$restored"
cp -a "$lab/combined/." "$restored/"
cat >> "$restored/postgresql.conf" <<CONF
port = 56433
summarize_wal = off
CONF
pg_ctl -D "$restored" -l "$lab/restored.log" -w -t 15 start >/dev/null
PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt <<'SQL'
SELECT pg_is_in_recovery();
SELECT id,value FROM marker ORDER BY id;
DO $$ BEGIN
  IF (SELECT string_agg(id||':'||value,',' ORDER BY id) FROM marker)
     IS DISTINCT FROM '1:final,3:second_increment' THEN
    RAISE EXCEPTION 'incremental restore result mismatch';
  END IF;
END $$;
SQL
printf 'lab_directory %s\n' "$lab"

The full backup contains row one with value full. The first increment, referenced to that full backup’s manifest, adds row two. The second increment references the first increment’s manifest, changes row one to final, deletes row two, and adds row three. This tests updates and deletion as well as the simple appearance of new rows.

Each input backup is verified before reconstruction. The first combine attempt intentionally supplies only the full backup and second increment. It must fail with a start-LSN mismatch, and the script checks that reason before printing missing_middle_rejected. No original backup is deleted to create the negative control. The failure demonstrates the dependency that a directory-count check would miss.

The complete combine command supplies the full backup, first increment, and second increment in order. Its synthetic full output is verified again, copied to a separate recovery directory, and started. Expected rows are 1|final and 3|second_increment; the SQL assertion checks that exact combined result. The instance has finished recovery, and the deleted row must not reappear.

pg_combinebackup validates relationships between inputs but does not replace each input’s integrity verification. The separate verification steps and real startup therefore establish different facts. The script uses ordinary copying, preserving the input backups while the recovered instance runs. All experimental servers stop on exit, and the printed directory retains logs and artifacts for inspection.

Budget the restore, not just the backup

Incrementals can reduce repeated copying of unchanged blocks, but restoration may require fetching, validating, and combining several artifacts before startup. Measure that complete path, including recovery and application checks. A faster daily backup does not establish that the incident recovery deadline is achievable.

Keep a dependency inventory alongside retention policy. PostgreSQL permits a synthetic full to become a later combine input, which can change the chain an operator needs to retrieve. Verify the new baseline before retiring its predecessors under the chosen policy. Changing checksum state or engine requirements can also require a fresh baseline. Rehearse a recent endpoint and older promised endpoints after rotation so storage savings do not silently shrink the recoverable window.

Further reading

Sources