MySQL
Replication Internals: Decoding the MySQL Binary Log Part 5: GTID_LOG_EVENT — The Global Transaction Identifier
10 min read
•
2 months ago

In this fifth post of our series, we decode the GTID_LOG_EVENT — the event that marks every transaction with a globally unique identifier.
Introduction
Every transaction in a GTID-enabled MySQL server begins with a GTID_LOG_EVENT (event type 33, 0x21). This event assigns a globally unique identifier to the transaction, consisting of:
- SID (Source ID): The UUID of the server that originally committed the transaction
- GNO (Group Number): A monotonically increasing sequence number
Together, they form a GTID like b8ae2fd2-3005-11f0-8be8-0242ac150002:12.
GTIDs simplify replication by removing the need to track binary log file names and positions. With GTIDs, you can:
- Set up new replicas without calculating binary log positions
- Failover to a new source without manual positioning
- Detect and prevent duplicate transaction execution
Note: MySQL 8.4 introduced the GTID_TAGGED_LOG_EVENT (event type 42, 0x2a), which uses a different serialization format and supports GTID tags. We'll cover that in a dedicated blog post. This post focuses on the classic untagged GTID_LOG_EVENT format.Event Location
In our binary log, we have multiple GTID_LOG_EVENTs — one for each transaction:
Let's decode the first one at position 197.
Common Header (19 bytes)
| Field | Bytes | Little-Endian | Value |
|---|---|---|---|
| Timestamp | 2d103568 | 0x6835102d | 1748308013 (2025-05-27 01:06:53) |
| Event Type | 21 | 0x21 | 33 (GTID_LOG_EVENT) |
| Server ID | 01000000 | 0x00000001 | 1 |
| Event Size | 4f000000 | 0x0000004f | 79 bytes |
| Next Position | 14010000 | 0x00000114 | 276 |
| Flags | 0000 | 0x0000 | No flags |
Reading the Raw Bytes
The event is 79 bytes: 19-byte common header + 42-byte post-header + 14-byte data body + 4-byte checksum.
GTID_LOG_EVENT Payload Structure
The GTID_LOG_EVENT payload consists of two parts: a fixed-size 42-byte post-header and a variable-length data body containing additional metadata fields.
The post-header fields are written by Gtid_log_event::write_post_header_to_memory():
| Field | Size | Description |
|---|---|---|
| GTID Flags | 1 byte | Bit flags — bit 0 (FLAG_MAY_HAVE_SBR): indicates the transaction may contain statement-based replication events |
| SID (UUID) | 16 bytes | The server UUID portion of the Transaction Source Identifier (TSID) |
| GNO | 8 bytes | Group Number (transaction sequence number within this SID) |
| Logical Clock Typecode | 1 byte | Always 0x02 — indicates logical clock timestamps follow |
| Last Committed | 8 bytes | Sequence number of the commit parent (for parallel replication) |
| Sequence Number | 8 bytes | This transaction's logical clock sequence number |
The data body fields are written by Gtid_log_event::write_body_to_memory():
| Field | Size | Description |
|---|---|---|
| Immediate Commit Timestamp | 7 bytes | Microsecond timestamp on the immediate server; MSB (Most Significant Bit, bit 55) flags whether Original Commit Timestamp follows |
| Original Commit Timestamp | 7 bytes | Conditional — only present if MSB of Immediate Commit Timestamp is set (i.e., transaction was replicated from a different server) |
| Transaction Length | 1–9 bytes | Packed integer — total transaction size in bytes, including this GTID event |
| Immediate Server Version | 4 bytes | MySQL version of the immediate server (e.g., 80040 = 8.0.40); MSB (bit 31) flags whether Original Server Version follows |
| Original Server Version | 4 bytes | Conditional — only present if MSB of Immediate Server Version is set |
| Commit Group Ticket | 8 bytes | Conditional — only present when Binlog Group Commit ticketing is active (value != 0) |
Let's decode the full payload (56 bytes, excluding checksum):
Post Header: Field-by-Field Decoding
GTID Flags: 01
This is a bit field stored in gtid_flags. Bit 0 is FLAG_MAY_HAVE_SBR — when set, it indicates the transaction may contain changes logged with statement-based replication (SBR). When cleared (0), the transaction contains only row-based events, which allows the replica applier to optimize its isolation level.
In MySQL 5.6, 5.7.0–5.7.18, and 8.0.0–8.0.1, this flag is always set. Starting in 5.7.19 and 8.0.2, it is cleared if the transaction only contains row events.
SID (UUID): 16 bytes
The UUID is stored as a raw 16-byte sequence (not little-endian — UUIDs use network byte order). Converting to standard UUID format: b8ae2fd2-3005-11f0-8be8-0242ac150002
This is the server_uuid of the MySQL instance where this transaction was first committed.
GNO: 0c00000000000000
The Group Number is 12. Combined with the UUID, the complete GTID is:
Logical Clock Typecode: 02
This typecode was introduced with the parallel replication logical clock in MySQL 5.7. Its value is always 2 (LOGICAL_TIMESTAMP_TYPECODE). If this byte is not present (in older binary logs), the last_committed and sequence_number fields are absent.
Last Committed: 0000000000000000
The last_committed value identifies the commit parent — the most recently committed transaction that this one depends on. A value of 0 means this is the first transaction in the binary log or has no dependencies.
Sequence Number: 0100000000000000
This transaction's sequence number is 1. Together with last_committed, these fields drive parallel replication on the replica — transactions with the same last_committed can be applied in parallel.
Data Body: Transaction Metadata
The remaining 14 bytes after the 42-byte post-header contain transaction metadata. Let's decode them field by field:
Immediate Commit Timestamp (7 bytes): c6 55 1b ae 13 36 06
The timestamp is stored as a 7-byte little-endian integer using int7store(), representing microseconds since the Unix epoch:
The MSB (bit 55) serves as a flag — see ENCODED_COMMIT_TIMESTAMP_LENGTH:
- 0: The transaction originated on this server —
original_commit_timestampequalsimmediate_commit_timestampand is not written - 1: The transaction was replicated — the next 7 bytes contain the
original_commit_timestamp
In our case, byte 6 is 0x06 = 0000 0110 in binary — bit 55 (the highest bit of the 7-byte value) is 0. This means the transaction originated locally, and no original_commit_timestamp follows.
Converting the timestamp: 1748308013569478 μs ÷ 1,000,000 = 1748308013.569478 seconds → 2025-05-27 01:06:53.569478 UTC — consistent with the header timestamp.
Transaction Length (packed integer): fc 05 01
This field uses MySQL's packed integer encoding (net_store_length()):
The total transaction length is 261 bytes — this includes the GTID_LOG_EVENT itself plus all subsequent events up to and including the transaction's terminal event (XID_EVENT or QUERY_EVENT with COMMIT/ROLLBACK).
We can verify: from position 197 (this GTID event) through 261 bytes lands at position 458 — which is exactly where the next GTID_LOG_EVENT begins. ✓
As a reminder from Part 1, the packed integer encoding works as follows:
| First Byte | Meaning |
|---|---|
0x00–0xFA (0–250) | The value itself (1 byte total) |
0xFC (252) | Next 2 bytes as a 16-bit integer |
0xFD (253) | Next 3 bytes as a 24-bit integer |
0xFE (254) | Next 8 bytes as a 64-bit integer |
Immediate Server Version (4 bytes): a8 38 01 00
Stored as a 4-byte little-endian integer using int4store():
The value 80040 encodes MySQL version 8.0.40 (major × 10000 + minor × 100 + patch).
The MSB (bit 31) serves as a flag, similar to the commit timestamp — see ENCODED_SERVER_VERSION_LENGTH:
- 0:
original_server_versionequalsimmediate_server_versionand is not written - 1: The next 4 bytes contain the
original_server_version
In our case, 0x000138a8 has bit 31 clear — both versions are the same. No original_server_version follows.
Commit Group Ticket (not present)
If the Binlog Group Commit (BGC) ticketing mechanism is active, an additional 8-byte commit_group_ticket would follow. It is only written when the value differs from the default kGroupTicketUnset (0). In our event, there are no remaining bytes — the ticket is not present.
Visual Breakdown
Parallel Replication: last_committed and sequence_number
The last_committed and sequence_number fields enable MySQL's parallel replication feature (introduced in MySQL 5.7). Here's how they work:
- sequence_number: A unique, monotonically increasing number assigned to each transaction
- last_committed: The sequence number of the most recent transaction that this transaction depends on
Transactions can be applied in parallel on a replica if they share the same last_committed value — meaning they don't depend on each other. For example:
In this example:
- B and C can run in parallel (both depend only on A)
- D must wait for C to complete
Comparing All GTIDs in Our Log
Let's look at all the GTID_LOG_EVENTs in our binary log:
| Position | GNO | GTID Flags | Last Committed | Sequence | Transaction |
|---|---|---|---|---|---|
| 197 | 12 | 0x01 (FLAG_MAY_HAVE_SBR) | 0 | 1 | CREATE TABLE person |
| 458 | 13 | 0x00 (row-only) | 1 | 2 | INSERT INTO person VALUES (1, 'Marcelo') |
| 768 | 14 | 0x00 (row-only) | 2 | 3 | UPDATE person SET name = 'Marcelo Altmann' |
| 1110 | 15 | 0x00 (row-only) | 3 | 4 | DELETE FROM person WHERE ID = 1 |
Each transaction has a sequential GNO, all from the same server UUID. Notice that the CREATE TABLE has FLAG_MAY_HAVE_SBR set (0x01) because DDL statements are always logged as statements. The DML transactions (INSERT, UPDATE, DELETE) have the flag cleared (0x00) — they contain only row-based events.
Also notice that each transaction's last_committed equals the previous transaction's sequence_number — they form a serial chain and cannot be parallelized. This is expected for a single-threaded workload on one server.
Try It Yourself
Output:
Notice that the first transaction (CREATE TABLE) has FLAG_MAY_HAVE_SBR set (0x01) while the remaining DML transactions show 0x00 (row-only). This is because DDL statements are always logged as statements, whereas our DML operations were logged using row-based replication.
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
Gtid_eventclass definition — Constants, field documentation, and the post-header layoutGtid_eventdeserialization constructor — How the event is read from a binary log bufferGtid_log_event::write_post_header_to_memory()— Post-header serialization (flags, UUID, GNO, logical clock)Gtid_log_event::write_body_to_memory()— Data body serialization (timestamps, transaction length, server versions)net_store_length()— Packed integer encoding implementation
What's Next?
Now that we understand how transactions are identified with GTIDs, we're ready to look at the actual transaction content. In the next post, we'll decode the QUERY_EVENT — the event that records SQL statements like DDL commands and transaction boundaries.
Next up: Part 6: QUERY_EVENT — DDL Statements and Transaction Boundaries
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