MySQL12 min read

MySQL Binlog Retention: binlog_expire_logs_seconds and Disk Pressure

binlog_expire_logs_seconds makes old files eligible for purge — it does not promise disk headroom, and it does not protect an offline replica. What pins purging, how to purge safely, and how to size retention from write rate.

Two incidents bookend this topic for me. The first was a 06:12 page on the first business day after a long weekend: the volume holding binary logs on our busiest primary hit 100%, the datadir itself growing at its usual 2GB a day. A backfill job had tripled binlog generation for three days, and the default 30-day expiry meant none of it was eligible for purge while the disk quietly filled. The second incident was the mirror image: a reporting replica that had been powered off for a week came back up and immediately died with error 1236 — could not find the first log file — because purging had removed binlogs it still needed. Age-based retention had cut in both directions, and both times it cut us.

binlog_expire_logs_seconds is one of those settings everyone has and few have reasoned about. It controls eligibility, not disk usage, and its interaction with replicas is asymmetric in a way that bites. Here is what it actually gates, what keeps files pinned on disk anyway, how to purge by hand without breaking replication, how to size retention from your write rate instead of a default, and the checklist for the classic complaint that expiry is set but nothing ever gets purged.

What does binlog_expire_logs_seconds actually control?

The age after which a binary log file becomes eligible for automatic purge — evaluated only when the server starts, when the binary log rotates, and when you purge explicitly, and never applied to the file currently being written. The 8.0 default is 2592000 seconds, thirty days, and zero disables automatic purging entirely; the old expire_logs_days variable is deprecated and maps onto this one. Rotation happens when the active file reaches max_binlog_size, on FLUSH LOGS or FLUSH BINARY LOGS, or at restart, and each rotation is a chance to re-evaluate which files have aged out. Two consequences follow. First, it is not a disk governor: if you write 500GB of binlog in three days with a seven-day expiry, you need disk for seven days of your peak write rate, not your average one. Second, it is not event-driven: nothing purges the moment a file crosses the age threshold. Purge waits for the next rotation or restart, which on a quiet server can be far away.

What keeps old binlogs pinned on disk?

An active dump thread reading an old file pins that file and everything after it — purge is strictly sequential from the oldest file — and an open file handle keeps the space allocated even after the index has forgotten the file. The pinned-by-replication case is the one to understand deeply. When a replica is connected, its Binlog Dump thread on the source is reading some file, and the purge logic will not remove a file a dump thread is reading, or anything newer. One connected replica running three days behind means three-plus days of binlog retained regardless of your expiry setting. CDC connectors and binlog-reading backup tools count too: a paused Debezium connector holds a position, and its dump thread pins from that file forward. Then the filesystem-level version: a tool holding an open file descriptor on a binlog file that purge has already removed from the index — the space comes back only when the descriptor closes, which is the lsof deleted-files check every Linux DBA learns the hard way. And the asymmetry that produces error 1236: an offline replica pins nothing at all. Purge protects connected consumers, not absent ones, so a replica offline longer than your retention comes home to a gap it can never replay.

How do you purge by hand without breaking replicas?

Check every replica's read position first, then purge to a point older than the oldest position anything still needs. PURGE BINARY LOGS TO 'binlog.000218' deletes the files listed in the binlog index before that file; PURGE BINARY LOGS BEFORE a datetime does the same by timestamp. The statement is safe to run while replicas replicate — it will not remove a file an active dump thread is reading, deleting earlier files and warning instead — and it never touches the active file. What it protects against is a connected reader; what it cannot protect against is the offline replica you forgot about, so the check comes first:

-- who is connected and reading binlogs right now?
SHOW PROCESSLIST;  -- look for "Binlog Dump" threads

-- on each replica: which source file is the IO thread reading?
SHOW REPLICA STATUSG

-- what exists, and how big is each file?
SHOW BINARY LOGS;

-- purge everything before a named file (never the active one)
PURGE BINARY LOGS TO 'binlog.000218';

-- or by timestamp
PURGE BINARY LOGS BEFORE '2026-07-20 00:00:00';

One thing never to do: delete binlog files from the shell. The .index file keeps referencing them, and the server errors when it next rotates or restarts and cannot open a listed file. If someone already did, the fix is editing the index file with the server stopped — ask me how I know — but the discipline is cheaper: binlog files enter and leave only through the server.

How do you size retention against lag SLAs and PITR?

Retention must cover your longest plausible replica outage plus the point-in-time recovery window your backups depend on, converted from measured write rate — not copied from a default. Measure first: sum the File_size column of SHOW BINARY LOGS over a representative day and you have binlog bytes per day, the number every other decision hangs on. Then the sizing is arithmetic. If your recovery runbook tolerates a replica being down for 48 hours before you re-provision it, retention must exceed 48 hours. If you take daily full backups and want PITR to any point in the last week, retention must exceed seven days so the binlogs from the oldest usable backup forward are still on disk — the replay mechanics are laid out in the XtraBackup PITR workflow. Disk is retention days times peak daily binlog times about 1.3 for headroom. My position on the default: thirty days is cargo cult for a busy primary. A host writing 40GB of binlog a day needs 1.2TB just to satisfy the default. I run three to seven days with tight growth alerting, because recovery windows longer than the backup cadence belong in the backup system's storage, not on the primary's hot volume.

Expire is set but nothing purges — what do you check?

Work the list in order: the runtime value, rotation activity, pinning consumers, relay logs, and open file handles. The runtime value first, because the classic failure is a my.cnf edit that never got a restart and never a SET PERSIST, so SHOW VARIABLES still shows thirty days. Check rotation next: purge is evaluated at rotation, so a server with low writes, a huge max_binlog_size, and 200 days of uptime evaluates rarely — FLUSH BINARY LOGS forces a rotation and an evaluation pass. Then look for pinning: a lagging connected replica or a stale connector holding a dump thread on an old file, exactly as above. Fourth, make sure the full disk is actually binlogs: on a replica, relay logs pile up when relay_log_purge is off or the SQL thread is stopped, and binlog expiry has nothing to do with relay logs. Fifth, the deleted-but-open case — purge removed the file from the index, some process holds the descriptor, df keeps reporting the space; restart or kill the holder. And the unglamorous sixth: someone pointed log_bin_basename at a different mount than the one you are watching, so your alert graph is measuring the wrong filesystem while the real binlog volume fills. Every one of these has burned an hour of my life; the list exists so it burns five minutes of yours.

How do you watch binlog growth before it pages you?

Graph the binlog directory's total bytes and its growth rate, and alert on projected time-to-full rather than a static percentage. SHOW BINARY LOGS hands you per-file sizes, so the simplest exporter sums them every minute; du over the binlog directory from the OS side works just as well. The alert that matters is a rate alert: at the current growth rate, will this volume fill within twice the time your slowest recovery runbook takes? If re-provisioning a replica takes four hours, you want the page at eight hours of headroom, not at 95% full. Two more alerts earn their keep: oldest retained binlog approaching your replica-outage tolerance — that is the error-1236 early warning — and any Binlog Dump thread reading a file older than your expiry setting, which is the pinning situation made visible. Growth anomalies on the source usually trace back to a job like the backfill that got me, and the general triage for a filling volume is covered in MySQL disk-full emergencies; binlog pressure is the most common cause I see in the wild, ahead of temp tables and ahead of actual data growth.

Where MonPG stands on MySQL

I build MonPG, so the honest version: MonPG monitors PostgreSQL today, and MySQL support is in active development, not shipped. Binlog volume, growth rate, retention headroom, and replication positions are exactly the signals the MySQL work is designed to graph together, because the incident is never one of them in isolation — it is the growth curve meeting the replica's read position on a shrinking disk. The MySQL monitoring (coming soon) page tracks that work as it lands. Until then the same evidence-first monitoring runs on the PostgreSQL side, and the rest of these MySQL field notes live on the blog.