MySQL
Replication Internals: Decoding the MySQL Binary Log Part 10: ROTATE_EVENT — Closing the File and Pointing at the Next One
8 min read
•
7 days ago

In this tenth post of our series, we decode the ROTATE_EVENT — the event that closes a binary log file and tells every reader, replica or local, where to look next.
Introduction
The ROTATE_EVENT (event type 4, 0x04) is the bookmark MySQL leaves at the end of a binary log file. It carries two pieces of information:
- The name of the binary log file to read next.
- The starting position within that next file (always
4, the byte right after the 4-byte magic number from Part 2).
Together those two fields are enough for any reader — a replica's I/O thread, a mysqlbinlog invocation, a CDC tool — to seamlessly continue from one file into the next. ROTATE_EVENT also has a second life on the wire: when a replica connects, the source sends an artificial ROTATE_EVENT first to tell the replica which file the upcoming events belong to. We'll see how to tell the two apart.
For our binlog.000024, the ROTATE_EVENT is the last event in the file — every byte before it has now been decoded across Parts 2–9.
Event Location
Reading the Raw Bytes
The event is 44 bytes: 19-byte common header + 8-byte post-header + 13-byte body (the filename) + 4-byte checksum.
Common Header (19 bytes)
| Field | Bytes | Little-Endian | Value |
|---|---|---|---|
| Timestamp | 39103568 | 0x68351039 | 1748308025 (2025-05-27 01:07:05) |
| Event Type | 04 | 0x04 | 4 (ROTATE_EVENT) |
| Server ID | 01000000 | 0x00000001 | 1 |
| Event Size | 2c000000 | 0x0000002c | 44 bytes |
| Next Position | c0050000 | 0x000005c0 | 1472 |
| Flags | 0000 | 0x0000 | No flags |
Cross-check: 1428 + 44 = 1472, and the file is exactly 1472 bytes long. ✓
The flags field is 0x0000. In particular, LOG_EVENT_ARTIFICIAL_F (bit 0x0020, defined in sql/log_event.h:277) is not set — this is a real on-disk ROTATE_EVENT, written by the server when it closed binlog.000024. We'll come back to artificial ROTATE_EVENTs in a moment.
ROTATE_EVENT Structure
The layout is documented in the `Rotate_event` class:
| Field | Size | Description |
|---|---|---|
| Post-Header (8 bytes) | ROTATE_HEADER_LEN | |
pos | 8 bytes | Byte position at which the first event begins in the next file (little-endian uint64) |
| Body | ||
new_log_ident | Variable | Name of the next binary log file — no null terminator, length runs to the end of the event |
The body is intentionally not null-terminated. The reader computes its length from the bytes still available after the post-header, minus the trailing 4-byte checksum. From Rotate_event::Rotate_event() in libs/mysql/binlog/event/control_events.cpp:
Two things worth noting:
- The fallback
pos = 4exists for legacy binary logs from MySQL versions whereROTATE_EVENThad no post-header. In binlog format version 4 (every modern MySQL), the post-header is always 8 bytes andposis read from it. The default value4is also what we expect to see: it'sLOG_EVENT_OFFSET, the byte right after the 4-byte magic number where the first event of any binary log lives. - The filename is capped at
FN_REFLEN - 1(511) bytes, but in practice it's tens of bytes — basenames likebinlog.000025.
The write side (Rotate_log_event::write() in sql/log_event.cc) mirrors the layout exactly:
int8store is MySQL's 8-byte little-endian integer store (the same primitive we've been seeing since Part 1), then the filename is appended as raw bytes.
Field-by-Field Decoding
Post-Header — pos: 0400000000000000
The first event in the next binary log starts at byte 4, immediately after the 4-byte magic number. As we saw above, this is LOG_EVENT_OFFSET and it's the value used for every "normal" rotation. (This field exists at all because the same ROTATE_EVENT structure is used when a replica's I/O thread has been told to start reading from a non-default offset — but that's a special case the server-side rotation never produces.)
Body — new_log_ident: 13 bytes
The next binary log is binlog.000025. The string runs from the start of the body to the byte before the 4-byte checksum — there is no null terminator and no length prefix.
Checksum: de7e7110
Visual Breakdown
When Is a ROTATE_EVENT Written?
The on-disk ROTATE_EVENT we just decoded is written by MYSQL_BIN_LOG::new_file_impl() — the function the server calls whenever it has decided to close the current binary log file and start a new one. There are several triggers that flow into that single code path:
| Trigger | Description |
|---|---|
max_binlog_size reached | The current file grew past the configured limit (default 1 GB); the server rotates as soon as the next transaction tries to append. |
FLUSH BINARY LOGS / FLUSH LOGS | A user (or a backup tool) explicitly requested a rotation. |
| Server startup | Each restart begins a new binary log file, so the previous file gets a closing ROTATE_EVENT pointing at the new one. |
RESET BINARY LOGS AND GTIDS | Wipes existing logs and starts fresh; the residual rotate marker preserves the file linkage during the reset. |
All four paths produce the same on-disk shape: post-header pos = 4, body = the next file's basename, no special flags. The construction at the call site looks like this:
LOG_EVENT_OFFSET is 4, and dirname_length(new_name) strips the directory so only the basename ends up in the body. The RELAY_LOG flag is a runtime-only bit on the in-memory class (used to tag relay-log rotations on the replica side); it is not written into the on-disk flags field, which is why our event shows 0x0000 even though it lives in a binlog (rather than relay log).
Two Kinds of ROTATE_EVENT
ROTATE_EVENT also shows up on the network protocol between source and replica, in two different forms. Telling them apart is mostly a matter of looking at the common header.
Real (on-disk) ROTATE_EVENT — the one we just decoded:
- Written by the source as the last event of a binary log file when it closes that file.
- Has a real timestamp (the rotation time).
- Flags =
0x0000(noLOG_EVENT_ARTIFICIAL_F). - The
next_positionfield in the common header points past the end of the current file. - Persisted to disk; it stays in the file forever.
Artificial ROTATE_EVENT — the very first event a replica's I/O thread receives when it (re)connects to a source. It exists so the replica knows which file the upcoming events belong to, before it has seen any FORMAT_DESCRIPTION_EVENT for that file:
- Generated on the fly in memory by the source's dump thread; never written to disk.
- Timestamp =
0(epoch) — this is a tell-tale sign you're looking at an artificial event. - Flag
LOG_EVENT_ARTIFICIAL_F(0x0020) is set in the common header. - The body still carries a filename and a position, but they describe where the dump thread will start streaming from, not a file boundary.
Both shapes parse with exactly the same code; only the surrounding context (and those two flag/timestamp tells) distinguishes them. The class doc on Rotate_event calls this out:
ROTATE_EVENT is generated locally and written to the binary log on the master. It is written to the relay log on the slave when FLUSH LOGS occurs, and when receiving a ROTATE_EVENT from the master. In the latter case, there will be two rotate events in total originating on different servers.
Try It Yourself
Output:
Cross-check against mysqlbinlog:
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
Rotate_event— class definition with the binary format documentationRotate_event::Rotate_event()— deserialization constructor (post-headerpos, then filename runs to the end of the event)Rotate_log_event::write()— serialization (int8storeforpos, raw bytes for filename)R_POS_OFFSET/R_IDENT_OFFSET— post-header field offsetsROTATE_HEADER_LEN = 8— post-header length constantRotate_event::DUP_NAME/RELAY_LOG— runtime-only flags on the in-memory class (not on-disk)MYSQL_BIN_LOG::new_file_impl()— constructs and writes the on-disk ROTATE_EVENT during rotationLOG_EVENT_ARTIFICIAL_F— the common-header flag set on artificial ROTATE_EVENTs sent to replicas
What's Next?
That's the last event in binlog.000024. Across nine posts (Parts 2 through 10), we've now manually decoded every byte of a real MySQL binary log — from the magic number to the closing ROTATE_EVENT — and seen how each event maps onto MySQL's source code in mysql-9.6.0.
But there's one event we deliberately deferred all the way back in Part 5: the GTID_TAGGED_LOG_EVENT introduced in MySQL 8.4. Tagged GTIDs use a different on-disk encoding from the classic untagged GTID we decoded, and they're worth a post of their own. That's the subject of Part 11.
Next up: Part 11: GTID_TAGGED_LOG_EVENT — Tagged GTIDs in MySQL 8.4+
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