MySQL10 min read

MySQL Redo Log Capacity: Checkpoint Tuning Without the Stalls

innodb_redo_log_capacity replaced the old log file settings in 8.0.30. Checkpoint age, the async and sync flush thresholds, and the counters that warn you before a write burst stalls the server.

Every write-heavy MySQL server I have run eventually produced the same mystery: throughput is fine for hours, then for thirty seconds it falls off a cliff, dirty pages spike, and everything recovers as if nothing happened. The first time, I blamed the disk. The second time, I blamed the batch job. The third time I finally looked at the right graph, checkpoint age pinned against redo log capacity, and the mystery ended. The server was not overloaded. It was out of redo log, and InnoDB was slamming on the brakes to protect itself.

Redo sizing is one of those settings that is invisible until the day it is the only thing that matters. Here is how it works on MySQL 8.0 and 8.4, what changed in 8.0.30, and what I monitor.

What the redo log is actually for

InnoDB is a write-ahead logging engine. When a transaction modifies a row, InnoDB changes the page in the buffer pool and appends a description of the change to the redo log; the dirty page itself is flushed to the data files later, whenever it is convenient. Commit durability comes from flushing the redo log, not the data pages, which is why commits are cheap even when the table is huge. Crash recovery replays redo records to rebuild any dirty pages that were lost.

The redo log is circular. Once the dirty pages covered by the oldest redo records have been flushed to the data files, that portion of the log can be reused. The point up to which the data files are current is the checkpoint, and the checkpoint advances as pages flush. This creates the fundamental constraint: you cannot reuse log space faster than you flush dirty pages. If writers generate redo faster than the flush machinery advances the checkpoint, the log fills, and InnoDB has no choice but to stop the world and flush aggressively. That stop is the cliff.

The 8.0.30 change: one capacity knob

For decades you sized redo with two settings, innodb_log_file_size and innodb_log_files_in_group, and changing them meant a restart plus a log rebuild dance that went wrong often enough to have its own folklore. MySQL 8.0.30 replaced both with a single variable, innodb_redo_log_capacity, default 100 MB, and made it dynamically resizable. The two legacy variables are deprecated, and the precedence is the opposite of what old habit suggests: when innodb_redo_log_capacity is defined, the legacy settings are ignored; only when it is left undefined does InnoDB derive the capacity from them, innodb_log_file_size times innodb_log_files_in_group. So the first thing I do on an inherited server is check which regime it is actually running under. New installs should set only innodb_redo_log_capacity and let the old names die.

Physically, the redo log now lives in the #innodb_redo directory inside the data directory, as 32 files named #ib_redoN, each sized as capacity divided by 32. Resizing online triggers a checkpoint and a rotation of those files, and Innodb_redo_log_resize_status tells you how it is going. The practical advice is to resize during ordinary traffic, watch the status, and avoid doing it in the middle of your peak write burst.

Checkpoint age and the flush thresholds

The number that governs everything is checkpoint age: the difference between the current log sequence number and the last checkpoint LSN, in other words how much redo has been generated that is not yet covered by flushed data pages. InnoDB watches it as a fraction of total capacity and escalates as it grows. Below the threshold, flushing is adaptive and gentle, paced by innodb_io_capacity and the adaptive flushing logic. When checkpoint age crosses roughly 81 percent of capacity, an internal, non-tunable threshold, InnoDB starts asynchronous flushing in earnest. Cross roughly 90 percent, another fixed internal line you cannot set, and InnoDB switches to synchronous flushing: user threads are held while dirty pages are forced out. That is the stall. Not a bug, not a slow disk, but the engine deliberately prioritizing crash safety over your throughput.

A too-small redo log makes this inevitable. The smaller the capacity relative to your write rate, the less headroom sits between "adaptive flushing is keeping up" and "sync flush is holding user threads." Batch jobs, big UPDATE sweeps, and bulk loads are the classic triggers: a job that generates redo at ten times the baseline rate can consume the entire remaining log in seconds.

The numbers that warn you early

MySQL 8.0.30 also added status variables that make this directly observable, no log parsing required:

SELECT variable_name, variable_value
FROM performance_schema.global_status
WHERE variable_name IN (
  'Innodb_redo_log_current_lsn',
  'Innodb_redo_log_checkpoint_lsn',
  'Innodb_redo_log_logical_size',
  'Innodb_redo_log_physical_size',
  'Innodb_redo_log_resize_status',
  'Innodb_redo_log_capacity_resized',
  'Innodb_os_log_written',
  'Innodb_log_waits'
);

Checkpoint age is current_lsn minus checkpoint_lsn; trend it as a percentage of Innodb_redo_log_capacity_resized, the status variable that reports the currently effective capacity limit, because the configured innodb_redo_log_capacity can be ahead of reality while a downward resize is still working through its checkpoint. If it routinely lives above half of capacity, your flushing is barely keeping up at baseline, and any burst will push you into the thresholds. Innodb_os_log_written is the redo generation counter; sample it on an interval to get bytes per second, which is the input to every sizing decision below. One caveat on Innodb_log_waits: it counts waits caused by a full in-memory log buffer, governed by innodb_log_buffer_size, not by the redo files themselves. It is a different knob, but it shows up in the same incidents, and any sustained increase means the log buffer is too small for your write bursts.

Sizing for write bursts

The sizing method I trust is empirical. Measure Innodb_os_log_written per second at your busiest hour, find the peak sustained burst, the worst fifteen to thirty minutes rather than the worst second, and size innodb_redo_log_capacity to hold at least that window, ideally an hour of peak generation. The old folk rule of "a quarter of buffer pool size" is not a terrible starting point on a write-heavy box, but it is a guess dressed as math; measured redo generation is the truth.

Bigger is not free, though the costs are smaller than they used to be. More redo means crash recovery has more log to scan and potentially replay, so recovery time after a crash grows with capacity. On modern versions with faster recovery this is far less scary than the old lore suggests, and the trade almost always favors headroom: a slightly longer recovery once a year versus sync-flush stalls every day. The one real constraint is that capacity alone does not flush pages. If innodb_io_capacity and innodb_io_capacity_max are set far below what your storage can do, InnoDB paces flushing as if your NVMe array were a spinning disk, and no redo size will save you. Set the I/O capacity near measured storage capability first, then size the log.

What the stall looks like from the outside

The signature is a sawtooth. Throughput runs flat, Innodb_buffer_pool_pages_dirty climbs, checkpoint age approaches capacity, adaptive flushing fails to keep pace, sync flush fires, and throughput collapses while pages are forced out; then the cycle repeats. Operators watching only QPS see random slowness. Operators watching checkpoint age see the whole story thirty seconds before it happens, which is the difference between paging and planning. If you have ever watched Innodb_buffer_pool_wait_free climb during these events, threads waiting because no clean page existed to evict into, that is the buffer pool echoing the same flush-bound condition, and the fix is redo and I/O capacity, not more memory. I wrote about the memory side of that confusion in the buffer pool sizing field guide.

Catching the cliff before the pager does

Redo capacity is the sort of signal a monitoring tool should trend so nobody has to, and it is high on the list for MonPG's MySQL monitoring, which is on the way — today the product watches PostgreSQL only. Concretely, that means checkpoint age graphed as a percentage of effective capacity rather than a raw LSN subtraction you redo in a spreadsheet, redo generation rate tracked against your measured peak windows, and threshold crossings alerted as the stalls they predict instead of as mystery latency after the fact. The PostgreSQL side already treats WAL exactly this way on the MonPG platform, and the comparison pages lay out how the two engines line up. Until the MySQL version ships, the status variables above are the whole dashboard: trend them anywhere durable, and the cliff stops being a surprise. The rest of the series lives on the blog.