The Runtime Theory
ApplicationDSAstorage

What happens when you COMMIT?

A step-by-step walk from BEGIN and dirty buffer-pool pages, through WAL buffer append, fsync, group commit, and the commit record, to the return to your client.

The Runtime Theory Team3 min read07 steps

layer stack

Application

HWHardware
KKernel
RTRuntime
APPApplication
SYSSystem
CLIClient
NETNetwork
TLSCrypto
SRVServer

adjacent altitudes in this subsystem are still being traced

trace spine

  1. 01 BEGIN: transaction context and snapshot
  2. 02 Writes land in the buffer pool
  3. 03 WAL records appended with LSNs
  4. 04 COMMIT: flush WAL to disk with fsync
  5. 05 Group commit amortizes the fsync
  6. 06 Commit state made visible (CLOG)
  7. 07 Data pages flushed later by checkpoints
On this page

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; ... ; COMMIT with 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.
sql
-- 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 total

31812 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.