COMMIT; looks like the end of the transaction. In reality it's the only part of the transaction where real physics happens: the moment data leaves the safety of RAM and must be on disk before the client hears "committed." The entire design of transactional storage — the write-ahead log, the buffer pool, fsync — is an answer to that single constraint.
Step 1 — BEGIN
BEGIN creates the transaction context: an XID is allocated, a snapshot is taken (MVCC: which other transactions' changes will be visible). No I/O — a BEGIN costs ~µs. Nothing durable yet.
Step 2 — writes land in the buffer pool
INSERT/UPDATE modify pages in the buffer pool — a shared cache of table/index pages, typically gigabytes. The page is updated in RAM only and marked dirty. If the server died right now, this change would be lost — that's fine, it's not committed.
Step 3 — WAL records are appended
Every change also writes a WAL record (write-ahead log) into the WAL buffer (Postgres: pg_wal, with an LSN per record; InnoDB: the redo log). This is the write-ahead rule: the log is written before the data is ever allowed to be durable. Log records are cheap to produce — sequential appends to RAM, ~100ns each.
Step 4 — COMMIT: the fsync
COMMIT appends one final record — the commit record, holding the transaction's LSN — and then the line is drawn: the WAL buffer must be flushed to disk and fsynced before the server answers "COMMIT." The fsync is the whole story:
- The OS has the log in its page cache; fsync forces it through the disk write path and waits for the device to confirm persistence (disk cache flush).
- A typical SSD fsync: ~0.5-5ms. A consumer SATA drive: up to 10-20ms.
- This is the reason databases commit in single-digit-ms at best on disk, no matter how fast the query itself was.
During this window the client is waiting and the transaction holds its locks. Latency per commit is dominated by the physical disk, not by the CPU.
Step 5 — group commit
One fsync per commit would cap throughput at ~1/fsync-latency commits per second (a few hundred). Instead, commit is a group operation: while the disk is flushing this commit's records, other transactions' commits keep appending to the WAL buffer, and the next fsync flushes all of them at once — one disk latency for dozens or hundreds of commits. Postgres batches the waiters: they all get "committed" when the shared flush completes. Group commit turns disk latency from a throughput cap into an amortized rounding error at high commit rates.
Step 6 — visibility
The commit record is durable; now the commit must become visible. Postgres writes the transaction's status to the commit log (CLOG) and updates the snapshot machinery: other transactions' snapshots now see the commit (if their isolation level permits). Readers that started before the commit still see the pre-commit state — MVCC holds.
Step 7 — data pages can wait
Notice what did not happen: the dirty data pages were not flushed. They sit in the buffer pool — possibly for minutes — until a checkpoint writes them out. That's the fundamental trade WAL buys: commit flushes a few KB of log instead of the whole dirty dataset. On crash, recovery replays the log forward (see the WAL-recovery trace) to rebuild what the data pages lost. The cost of not flushing data is paid later, at checkpoint time and at recovery.
What it costs
- Bare
BEGIN; ... ; COMMITwith everything cached: ~1-10ms, ~95% of it the fsync. - With
synchronous_commit = off: ~100-500µs — the fsync is skipped; a crash can lose up to ~0.5s of commits (the WAL writer's flush delay). - With group commit: amortized — 1,000 commits/sec can cost barely more than 100/sec.
- The hidden tax: every commit extends the WAL; checkpoints and WAL archiving later have to move it.
-- watch commit behavior directly
SELECT * FROM pg_stat_wal \g
wal_write | 1243811 -- writes of WAL to disk
wal_fsync | 31812 -- real fsyncs: 39 commits per fsync
wal_write_time | 11234.4 -- ms total31812 fsyncs for 1.2M WAL writes — that's group commit working: most commits rode someone else's fsync. The rule for low commit latency: keep transactions short (locks held during the fsync wait block others), let group commit batch naturally, and never put the WAL on the same disk you're hammering with data writes.