Snapshot & Binlog Coordination for Consistent PITR Baselines

A point-in-time recovery is a two-part chain: a base backup that establishes a known coordinate, and a run of binary logs that carries state forward from that coordinate to the target. The chain is only sound if the two parts meet exactly — the binlog replay must begin at precisely the GTID set the snapshot captured, with neither a gap (missing transactions between the snapshot and the first archived segment) nor an overlap (transactions present in both, which double-apply). Naive approaches get this wrong in a specific, dangerous way: they take a snapshot and separately note “the current binlog position,” but if any transaction commits between those two operations, the recorded coordinate no longer matches the snapshot’s contents, and every future recovery built on that backup is off by that delta. This guide defines how to capture a snapshot and its GTID coordinate as a single consistent fact, and how to prove the archived chain begins exactly where the snapshot ends. It is the baseline half of the recovery workflow in Point-in-Time Recovery Workflow Automation and the mirror of the anchoring described in Base Backup Integration for PITR.

Visual Overview

Snapshot and binlog coordination on the GTID axisA horizontal GTID axis. A base snapshot bar covers transactions 1 through N and records gtid_purged equal to 1 to N. The archived binlog chain bar begins at N plus 1 and continues to the live tail. The seam between them is labelled: correct when the chain starts at N plus 1, a gap if it starts later, an overlap if it starts at or before N.GTID transaction_id →Base snapshotgtid_purged = UUID:1-NArchived binlog chainUUID:(N+1)-tailthe seam: exactly N | N+11Ntail
A sound baseline is a clean seam: the snapshot records gtid_purged = 1..N and the archived chain begins at N+1 — no gap, no overlap.

Core Concept & Prerequisites

The consistency guarantee hinges on capturing the snapshot and its GTID coordinate atomically with respect to the transaction stream. Physical backup tools do this correctly by design: a consistent snapshot brackets a moment in the redo/undo state and records the exact gtid_executed at that moment in a metadata file — mysqldump --single-transaction --source-data=2 writes the coordinate inside the same consistent read view, and physical tools write it to xtrabackup_binlog_info. The failure mode is doing it by hand: FLUSH TABLES WITH READ LOCK, note the position, snapshot, unlock — which is correct only if nothing slips between the lock acquisition and the position read, and which stalls writes for the snapshot duration.

You need MySQL 8.0.22+ with gtid_mode=ON (so the coordinate is a portable GTID set rather than a host-local file:position), Python 3.10+, a snapshot mechanism (a physical tool such as Percona XtraBackup, an LVM/cloud volume snapshot, or a logical dump), and the archived chain from Automated Binlog Archiving to Object Storage. The coordinate you capture must be expressed as a GTID set, because a file:position coordinate is meaningless on a recovery target with a different server_id — the portability argument is developed in GTID Tracking & Enforcement.

Production-Grade Python Implementation

The coordinator captures a consistent snapshot coordinate and immediately validates it against the archived chain, so a misaligned backup is caught at creation time rather than at recovery time. It reads the GTID coordinate the backup tool recorded, then asserts the archive’s first segment begins exactly one transaction later.

# baseline/coordinate.py — Python 3.10+
from __future__ import annotations

from dataclasses import dataclass


@dataclass(slots=True, frozen=True)
class Baseline:
    backup_id: str
    base_gtid: str          # gtid_executed captured with the snapshot, e.g. "UUID:1-400000"


def parse_xtrabackup_coord(info_text: str) -> str:
    """Extract the GTID set from an xtrabackup_binlog_info / xtrabackup_info field."""
    for line in info_text.splitlines():
        # xtrabackup_binlog_info: "mysql-bin.000123<TAB>948<TAB>UUID:1-400000"
        parts = line.split("\t")
        if len(parts) == 3 and ":" in parts[2]:
            return parts[2].strip()
    raise ValueError("no GTID coordinate found in backup metadata")


def assert_clean_seam(base_gtid: str, first_segment_gtid: str, conn) -> None:
    """Prove the archive begins exactly where the snapshot ends: no gap, no overlap."""
    cur = conn.cursor()
    # Gap check: everything the snapshot covers must be a subset of base ∪ chain start.
    cur.execute("SELECT GTID_SUBTRACT(%s, %s)", (first_segment_gtid, base_gtid))
    only_in_chain = cur.fetchone()[0] or ""
    cur.execute("SELECT GTID_SUBTRACT(%s, %s)", (base_gtid, first_segment_gtid))
    only_in_base = cur.fetchone()[0] or ""
    cur.close()
    # A clean seam: the chain's first interval starts one past the base's last.
    if _overlaps(base_gtid, first_segment_gtid):
        raise ValueError(f"overlap at seam: transactions in both base and chain")
    if _has_gap(base_gtid, first_segment_gtid):
        raise ValueError(f"gap at seam: missing transactions between base and chain")

The assert_clean_seam call is what makes a backup usable, not merely present. Running it at backup-creation time turns “is this backup recoverable?” from a recovery-day discovery into a creation-day gate. The full standalone check, including how to compute the interval arithmetic robustly, is developed in Validating GTID Overlap Between a Base Backup and the Binlog Chain, and the XtraBackup-specific coordinate handling is in Aligning XtraBackup Snapshots with gtid_purged.

Configuration Reference

Setting / artifactWhereRecommendedWhy it matters for PITR
gtid_modeprimaryONThe coordinate must be a portable GTID set, not a host-local file:position.
--source-data=2mysqldumponRecords the binlog coordinate inside the consistent read view as a comment.
xtrabackup_binlog_infophysical backuppreservedCarries the exact GTID set the snapshot captured.
snapshot isolationvolume snapshotcrash-consistent + FTWRL-freePrefer redo-consistent physical backups over lock-and-note.
binlog_expire_logs_secondsprimary≥ backup interval + marginGuarantees the seam segment still exists locally to be archived.
seam validationbackup jobfail the backup on gap/overlapCatches misalignment at creation, not at recovery.

Validation & Verification Gates

Error Handling & Failure Modes

A file:position coordinate on a GTID target cannot be used to seed gtid_purged; you get ERROR 1840/3546 or a silently wrong floor. Always capture and store the GTID set.

Overlap at the seam produces ERROR 1062 (duplicate key) or a gtid_next rejection during replay, because the first archived transaction was already in the snapshot. This comes from recording the coordinate before the snapshot’s consistent point; capture them together.

Gap at the seam is the more dangerous case because it can be silent: replay succeeds but skips transactions that committed between the snapshot and the first archived segment, so the recovered database is missing writes with no error. Only the interval-subtraction check catches it, which is why seam validation must run on every backup.

Observability & Alerting

Record, per backup, the captured GTID coordinate and the seam-validation result, and alert if any backup lands with a gap or overlap. Track the seam margin — the number of transactions of headroom between the backup coordinate and the oldest still-archived segment — because as that margin shrinks toward zero you are one retention cycle away from an unrecoverable gap. Cross-reference this with the archiving-lag metric from Deriving RPO Targets from Binlog Archiving Lag: a healthy baseline needs both a clean seam and an archive that keeps pace.

Frequently Asked Questions

Why not just take a snapshot and run SHOW BINARY LOG STATUS right after?

Because any transaction that commits between the snapshot’s consistent point and the status read makes the recorded coordinate wrong by exactly that delta — a gap or overlap baked into every future recovery. A consistent backup tool records the coordinate inside the same read view as the data, which is the only way to guarantee they match.

GTID set or file:position for the coordinate?

A GTID set. A file:position is host-local and meaningless on a recovery target with a different server_id, so it cannot seed gtid_purged. A GTID set is portable and is exactly what the replay floor requires.

What is a “clean seam” in one sentence?

The archived chain begins at the transaction immediately after the last one the snapshot captured — no transaction is missing between them (a gap) and none is present in both (an overlap).

Do I need to validate the seam on every backup, or once?

Every backup. Each new base backup establishes a new coordinate that must line up with the archive as it exists at that moment; retention, rotation, or an archiving stall between backups can break alignment that was fine last week.

Back to Point-in-Time Recovery Workflow Automation.