The two most quoted settings in MySQL operations are sync_binlog=1 and innodb_flush_log_at_trx_commit=1, and a surprising number of the people quoting them cannot tell you what the pair actually bought. I learned the real meaning the way these lessons usually arrive: during a failover drill, on a cluster someone had temporarily relaxed for a load test six weeks earlier. The machine crashed hard, exactly as planned. What was not planned was the forty seconds of acknowledged orders that simply were not there when the replica took over. No error had ever been returned to the application. Every one of those commits had been acknowledged to a client. They just had not been durable, because both knobs were sitting at their fast values and nobody had written that down.
This is the guide I wish had been taped to that load test plan: what the two durability knobs actually promise, how binlog group commit makes the strongest promise affordable, and how to decide, deliberately and in writing, where relaxing is acceptable.
Two logs, one commit
With binary logging enabled, MySQL commits every InnoDB transaction to two different logs, and understanding the durability knobs starts with why. The redo log is InnoDB's own: a physiological record of page changes that lets the engine crash-recover its tablespaces. The binary log is the server's: a logical record of committed changes that replicas consume and point-in-time recovery replays. They live in different layers, contain different content, and if the server could crash after making one durable but not the other, the two would disagree about what committed, and replicas would silently drift without a single error.
InnoDB and the binlog prevent this with an internal two-phase commit. The transaction is first prepared in InnoDB, with its redo written and flushed. Then the binlog is written and synced. Only then is the transaction marked committed inside the engine. During crash recovery, the server walks the binlog and reconciles: a transaction found prepared in the engine and present in the binlog is committed; one missing from the binlog is rolled back. That choreography is why flush order and flush discipline are not implementation trivia. They are the durability contract itself.
What each knob actually promises
innodb_flush_log_at_trx_commit controls the redo side. At 1, the redo for your transaction is flushed to durable storage at commit time, as part of the prepare phase. At 0, a background thread writes and flushes roughly once per second, and commits return without touching storage at all. At 2, redo is written to the operating system's page cache at commit and fsynced about once a second, so a mysqld crash loses nothing but an OS crash or power loss can take the last second or so of acknowledged commits. The practical difference between 0 and 2 is which crash scenario eats the data.
sync_binlog is the same idea for the binary log. At 0, MySQL never fsyncs the binlog and the operating system flushes whenever it feels like it, which makes your loss window whatever the OS happened to be holding. At 1, the binlog is synced once per commit group. At any higher N, it syncs once per N groups, trading an explicitly bounded risk window for fewer syncs. MySQL 8.0 defaults to 1, but the 5.6-era default was 0, and a lot of older fleets got quietly surprised by upgrades nobody audited for this.
Running both at 1, the famous one-one, is the full guarantee: an acknowledged commit survives OS crash and power loss in both logs, assuming storage honors fsync. A RAID controller with a volatile write-back cache that lies about flushes voids that contract from below; battery- or capacitor-backed cache is the classic fix, not anything in my.cnf.
Group commit: one fsync for many transactions
Strict per-commit syncing puts hard arithmetic on durable throughput: roughly one commit per fsync latency. When an fsync costs a millisecond, common on network block storage, the ceiling lands near a thousand durable commits per second no matter how many cores the box has. Binlog group commit is the answer: transactions queue, a leader flushes redo for the batch, writes the batch's binlog events, pays a single binlog fsync, and the transactions commit in order, through three stages, flush, sync, and commit, with followers piggybacking on each stage leader. The fsync count stops scaling with commits and starts scaling with time.
Two companion knobs shape the batching: binlog_group_commit_sync_delay adds a deliberate pause, in microseconds, before the sync stage so the queue can collect more transactions, and binlog_group_commit_sync_no_delay_count cuts the wait short once enough transactions are queued. The knob buys batching with latency, so it is a throughput dial: useful for many tiny commits, pointless or harmful where batching already happens or latency matters.
Batch depth is the number to watch, and here stock MySQL is less convenient than the forks: it exposes no group-size status counter. Percona Server added Binlog_commits and Binlog_group_commits for exactly this, and their ratio is your average group size. On stock builds you read the same truth from the binlog itself: mysqlbinlog -v stamps every transaction with last_committed and sequence_number, transactions sharing a last_committed value committed as one group, so sequence numbers divided by distinct last_committed values over a busy window is the average group size. First, though, confirm the batching knobs are where you think they are:
SELECT @@binlog_group_commit_sync_delay AS sync_delay_us,
@@binlog_group_commit_sync_no_delay_count AS no_delay_count,
@@sync_binlog AS sync_binlog;
If the average group size hovers near one, every commit pays for its own fsync and group commit gives you nothing: either concurrency is genuinely low, or something upstream is serializing commits, and both are worth knowing before you touch a knob.
The cost you cannot remove
Even perfectly batched, a durable commit pays for two storage synchronizations: the redo flush at prepare and the binlog sync. The commit-stage redo write is batched lazily, so the realistic count is two, not three, but two is the floor. That double payment is the price of the two-log architecture. PostgreSQL gets away with a single WAL because its replication and crash recovery consume the same stream; MySQL's crash-safe replication needs both logs durable because the binlog is a separate product feeding separate consumers.
The floor shows up directly in latency: a durable commit cannot return faster than roughly twice your storage's fsync latency. On local NVMe an fsync is tens of microseconds and the floor is invisible. On cloud volumes it can run one to two milliseconds, and the floor becomes your p99. When commit latency is the complaint, fsync latency is the suspect, and no my.cnf value changes it. If dirty-page flushing is the other half of the story on your systems, and it usually is, the memory side of that pipeline is covered in the InnoDB buffer pool field guide.
What 1,1 guarantees, and what it does not
The guarantee is narrow: an acknowledged commit survives a crash of the source machine, present in both logs. It says nothing about whether any replica has seen the transaction. Asynchronous replication ships binlog events after commit, so promoting a replica after a source crash can lose the last transactions even on a perfect 1,1 source. Semi-synchronous replication closes most of that window by waiting for a replica's receipt acknowledgment before answering the client, at real latency cost, and it is a separate decision solving a separate problem.
The relaxed profiles deserve the same precision. With innodb_flush_log_at_trx_commit=2 and sync_binlog=0, everything rides the OS cache: a mysqld crash loses nothing, a machine crash loses roughly the last second of commits, and more when process scheduling delays the nominal once-per-second flush, while with sync_binlog=0 the binlog side of the loss is whatever the OS happened to be holding, which has no bound at all. Acceptable for rebuildable analytics replicas, caches, and dev boxes; unacceptable for anything involving money, anything a backup chain treats as the source of truth, or any cluster whose replicas might be promoted, which describes most clusters eventually.
Two more settings complete crash safety on replicas: relay_log_info_repository and master_info_repository must be TABLE, the 8.0 default, so a replica crash cannot corrupt position bookkeeping into re-applying or skipping transactions, and gtid_mode=ON makes promotion and re-pointing deterministic. The old FILE repositories are how supposedly crash-safe clusters re-applied a few thousand transactions after a power event.
How I set it and what I watch
My fleet default is 1,1, relaxed per cluster only with a written reason. binlog_group_commit_sync_delay gets touched only after the measured group size shows batching is not happening on its own. I watch three things: commit latency, the group-size ratio, and storage sync latency from the file I/O waits on the binlog and redo files. Falling group size with rising commit latency means batching broke; rising sync waits mean storage degraded. From the outside both look identical: MySQL got slow.
Where MonPG stands on MySQL
I build MonPG, so I will be straight about where it stands: it monitors PostgreSQL today, and MySQL support is still being built, not shipped. The durability lesson in this guide is part of the roadmap: a monitoring tool should know which clusters run relaxed flush settings, notice when group size collapses toward one, and graph binlog and redo sync latency next to commit latency so the storage-versus-engine question answers itself. The MySQL monitoring (coming soon) page is where that work shows up as it lands. Until it does, the PostgreSQL product already carries the equivalent WAL-durability workflow on the PostgreSQL side, the comparison pages show the shape of it, and the rest of these MySQL field notes are on the blog.