MySQL
Replication Internals: Decoding the MySQL Binary Log Part 4: PREVIOUS_GTIDS_LOG_EVENT — Tracking Transaction History
7 min read
•
2 months ago

In this fourth post of our series, we decode the PREVIOUS_GTIDS_LOG_EVENT — the event that tracks which GTIDs were recorded in prior binary log files, enabling replicas to determine their starting point.
Introduction
When MySQL rotates to a new binary log file, it needs to record which transactions have already been committed in previous files. This is the job of PREVIOUS_GTIDS_LOG_EVENT—it appears near the beginning of every binlog file (right after the FORMAT_DESCRIPTION_EVENT) and contains the complete set of GTIDs from all prior logs.
This event is critical for GTID-based replication. When a replica connects to a source, it sends its own GTID set (the transactions it has already executed). The source compares this against its PREVIOUS_GTIDS to determine the starting point—which transactions the replica still needs. Without this event, the source would have to scan through all binary logs to figure out what the replica is missing.
MySQL 8.4 introduced tagged GTIDs, which add an optional tag to the traditional UUID:transaction_id format (e.g., UUID:mytag:1-5). This required a new binary encoding for PREVIOUS_GTIDS_LOG_EVENT while maintaining backward compatibility with older servers.
Event Header Recap
From Part 2, we know that every event starts with a 19-byte common header. Let's read the PREVIOUS_GTIDS_LOG_EVENT header from our MySQL 8.0.40 binary log:
The hex bytes are: 6e0f3568 23 01000000 47000000 c5000000 8000
| Field | Bytes | Value | Meaning |
|---|---|---|---|
| Timestamp | 6e0f3568 | 1748307822 | 2025-05-27 01:03:42 |
| Event Type | 23 | 35 | PREVIOUS_GTIDS_LOG_EVENT |
| Server ID | 01000000 | 1 | Server ID |
| Event Size | 47000000 | 71 bytes | Total event size |
| Next Position | c5000000 | 197 | Next event starts at byte 197 |
| Flags | 8000 | 0x0080 | LOG_EVENT_IGNORABLE_F |
The event type 0x23 (35) confirms this is a PREVIOUS_GTIDS_LOG_EVENT. The payload size is 71 - 19 (header) - 4 (checksum) = 48 bytes.
PREVIOUS_GTIDS_LOG_EVENT Payload Structure
The payload structure depends on whether the binary log uses the untagged (classic) or tagged (MySQL 8.4+) GTID format. Before we parse the data, we need to detect which format we're dealing with.
Detecting Tagged vs. Untagged Format
The first 8 bytes of the payload encode both the SID count and the format type. The key is byte 7 (the 8th byte, using 0-based indexing):
| Byte 7 Value | Format | MySQL Version |
|---|---|---|
0x00 | Untagged (classic) | < 8.4 |
0x01 | Tagged | 8.4+ |
Let's look at the first 8 bytes from our MySQL 8.0 payload:
And from a MySQL 9.6 payload with tagged GTIDs:
The encoding differs between formats:
Untagged format:
Tagged format:
Why this detection is safe: For byte 7 to equal 0x01 in untagged format, n_sids would need to be at least 72 quadrillion (0x0100000000000000). However, MySQL internally uses rpl_sidno (defined as int in sql/rpl_gtid.h) which is a 32-bit signed integer—limiting the maximum to about 2.1 billion SIDs. Even this theoretical limit is astronomically higher than any real deployment. So if byte 7 is 0x01, it's definitively the tagged format indicator.
This encoding comes from MySQL's sql/rpl_gtid_set.cc:
Now that we know how to detect the format, let's decode each one.
Untagged Format Structure
| Field | Size | Description |
|---|---|---|
| n_sids | 8 bytes | Number of SIDs (Server UUIDs) in this event |
| SID entries | Variable | One entry per SID |
Each SID entry contains:
| Field | Size | Description |
|---|---|---|
| UUID | 16 bytes | Server UUID (raw bytes, not formatted) |
| n_intervals | 8 bytes | Number of transaction ranges for this UUID |
| intervals | 16 bytes each | start (8 bytes) + end (8 bytes), end is exclusive |
Let's read the payload:
Field-by-Field Decoding (Untagged Format)
n_sids: 0100000000000000
There is 1 SID (Server UUID) in this event. As we discussed above, byte 7 is 0x00, confirming this is the untagged format.
UUID: b8ae2fd2300511f08be80242ac150002
This is a raw 16-byte UUID. To convert to the standard formatted string, we group the bytes:
UUID: b8ae2fd2-3005-11f0-8be8-0242ac150002
This is the server_uuid of the MySQL instance that created the transactions.
n_intervals: 0100000000000000
There is 1 interval (transaction range) for this UUID.
interval[0].start: 0100000000000000
The interval starts at transaction number 1.
interval[0].end: 0c00000000000000
The interval ends at 12. But wait — this is an exclusive end! The actual last transaction is 12 - 1 = 11.
Decoded Result
Combining all fields:
This means the binary log files prior to this one contained transactions 1 through 11 from this server.
Tagged Format: A MySQL 9.6 Example
Let's examine a PREVIOUS_GTIDS_LOG_EVENT from MySQL 9.6.0 that contains two TSIDs—one without a tag and one with the tag "mytag":
Tagged Format Structure
| Field | Size | Description |
|---|---|---|
| n_tsids | 8 bytes | Format indicator + count (encoded) |
| TSID entries | Variable | One entry per TSID |
Each TSID entry contains:
| Field | Size | Description |
|---|---|---|
| UUID | 16 bytes | Server UUID |
| tag_length | 1 byte | actual_length * 2 (0 = no tag) |
| tag | Variable | Tag string (if tag_length > 0) |
| n_intervals | 8 bytes | Number of transaction ranges |
| intervals | 16 bytes each | start (8 bytes) + end (8 bytes) |
Field-by-Field Decoding (Tagged Format)
n_tsids: 0102000000000001
There are 2 TSIDs in this event, using the tagged format.
TSID #1 (Untagged Transaction)
UUID: 5577890402991f1b1b84ef0c4956feb
UUID: 55778904-0299-11f1-b1b8-4ef0c4956feb
tag_length: 00
Tag length is 0, meaning this TSID has no tag. Even in tagged format, individual GTIDs can be untagged.
n_intervals: 0100000000000000
There is 1 interval for this TSID.
interval[0].start: 0100000000000000
Start: 1
interval[0].end: 0e00000000000000
End: 14 (exclusive), so actual end is 13.
TSID #1 Result
TSID #2 (Tagged Transaction)
UUID: 5577890402991f1b1b84ef0c4956feb
Same UUID as TSID #1 — this is the same server, but with a different tag.
tag_length: 0a
The tag is 5 characters long. The stored value is actual_length * 2.
tag: 6d7974616
Tag: "mytag"
n_intervals: 0100000000000000
There is 1 interval.
interval[0].start: 0100000000000000
Start: 1
interval[0].end: 0300000000000000
End: 3 (exclusive), so actual end is 2.
TSID #2 Result
Complete Decoded Results
MySQL 8.0.40 (Untagged):
MySQL 9.6.0 (Tagged):
Try It Yourself
Here's a Python script to parse PREVIOUS_GTIDS_LOG_EVENT payloads:
Note: The binary log files used in this series (binlog.000024,binlog_gtid_tag.000001, and others) are available at github.com/altmannmarcelo/presentations/tree/main/binlog.
References
- sql/rpl_gtid_set.cc —
Gtid_set::encode()implementation - MySQL Tagged GTIDs Documentation
- Daniël van Eeden's Blog: MySQL GTID tags and binlog events
What's Next?
Now that we understand how MySQL tracks GTID history across binary log files, we're ready to look at the event that assigns GTIDs to individual transactions. In the next post, we'll decode the GTID_LOG_EVENT — the event that marks the beginning of each transaction with its globally unique identifier.
Next up: Part 5: GTID_LOG_EVENT — The Transaction Identifier
This series is based on a presentation given at the MySQL Online Summit. The goal is to help MySQL users understand what goes under the hood of replication by manually decoding binary log files.
Authors