What "Committed" Really Means
When COMMIT returns, Postgres has written the transaction's WAL records and asked the operating system to flush them to durable storage, and by default it waits for that flush before answering. The waiting is the guarantee. Every setting that makes commits faster works by weakening that one sentence, and the settings differ in which part of it they weaken.
Being pedantic about the difference is the point of the topic, because the two parameters people reach for have similar names, similar-sounding documentation and completely different blast radii. One of them costs you a fraction of a second of committed transactions after a power failure. The other costs you the database. Cartwheel makes the first choice deliberately, per code path, and has never made the second.
The Default Contract
synchronous_commit = on is the default, and it means the commit does not return until the transaction's WAL records have been flushed to local durable storage. A transaction that reported success therefore survives an operating-system crash or a power loss, because the records describing it are on the device rather than in a buffer somebody was about to write. That is the sentence every other claim in this chapter is measured against: a base backup plus archived WAL can only ever recover transactions that were durable when they were acknowledged.
The cost is a flush per commit, but not one flush per transaction under load. Concurrent committers share flushes, and commit_delay, zero by default, exists to deliberately pause before a flush so that more transactions join the same one, which is worth something only when the system is already busy enough for others to be arriving. What a flush actually costs is a property of the storage rather than of Postgres, and measuring it is the last section here.
synchronous_commit = off
Set it to off and the commit returns without waiting; the WAL writer pushes the records out shortly afterwards. The exposure is bounded and the manual states the bound: the maximum delay is three times wal_writer_delay, which defaults to 200 milliseconds, so the window of acknowledged-but-not-yet-durable work is at most about 600 milliseconds. On the delivery_events ingest path, running at roughly four million rows a day, that is a few hundred telemetry rows.
What is not at risk is the database. The manual is explicit that, unlike fsync, this setting creates no risk of database inconsistency: a crash may lose some recent allegedly-committed transactions, but the resulting state is the same as if those transactions had aborted cleanly. Nothing is half-applied, no page disagrees with the log, and recovery behaves normally. A delivery event survives that outcome and a payment does not, so the setting is available at role, session and transaction scope rather than only in the configuration file.
fsync = off Is a Different Thing Entirely
fsync is on by default and it controls whether Postgres issues fsync() calls at all, to make sure updates are physically written to disk. Switch it off and the server stops forcing anything anywhere — WAL and data files alike. The write-ahead ordering that makes replay meaningful depends on the log genuinely reaching the device before the pages it describes; remove the forcing and a crash can leave data files in a state the log cannot reconcile. What you get after that crash is a cluster that does not start and cannot be repaired from anything on that machine.
The manual's own advice is the useful version: where write latency is the problem, turning off synchronous_commit for the non-critical transactions delivers much of the same benefit without any of the corruption risk. So any recommendation to set fsync = off on a system holding data somebody cares about is a recommendation about a benchmark that has been repeated out of context. On a load-generation box or a disposable test cluster that gets rebuilt from a script, it is a perfectly reasonable way to stop measuring the disk.
The Remote Levels
Once a synchronous standby exists, the same dial extends across the network and gains three more positions. remote_write returns when the standby has received the commit record and written it to its file system, which is not the same as durable. on waits until the standby has received it and flushed it to durable storage. remote_apply waits until the standby has applied it, so a query routed there immediately afterwards will see the row. local is the opposite end: flush here and do not wait for anyone.
Each step outward adds a network round trip to every commit on the primary, which is the part that surprises people who set synchronous_standby_names and then investigate a latency regression that is simply the physics of the link. Chapter 13 owns synchronous replication as a subject, including how many standbys must answer and what happens when none can. What belongs here is the shape of the dial: five values, ordered from a local flush the commit does not wait for out to a commit that waits for replay on another machine.
fsync = off is not a position on this dial. It stops the server forcing anything anywhere, and a crash can then leave data files the log cannot reconcile.
Choosing per Transaction
One global value is a poor answer to a question that has several correct ones, because a database rarely holds data of a single value. Cartwheel's payment and order-placement paths get the full guarantee without argument. The delivery_events ingest is telemetry with a replay upstream, and losing 600 milliseconds of it during a power failure changes nothing anybody will notice. synchronous_commit is not a server-only setting: a role, a session or a single transaction can carry its own value, and the one that applies is the value in force when the transaction commits.
BEGIN; SET LOCAL synchronous_commit = off; -- telemetry: 600 ms is affordable INSERT INTO delivery_events (order_id, event_type, occurred_at, payload) SELECT ...; COMMIT; -- the checkout path changes nothing and keeps the default: BEGIN; INSERT INTO orders (public_id, customer_id, placed_at, status, total) ...; COMMIT; -- waits for the flush, as promised
SET LOCAL scopes the relaxed setting to that one transaction, so the session returns to the default the moment it ends and nothing leaks into the next statement the pooler hands out. The mechanism is three words long; the engineering is the classification. Somebody has to decide which paths are which, write the list down where the next person will find it, and revisit it when a table changes meaning. A queue table that becomes the source of truth for refunds has changed durability class, and its SET LOCAL line will still say off.
What Storage Must Provide
All of this assumes fsync() tells the truth. A consumer SSD with a volatile write cache, a RAID controller whose battery died two years ago, a hypervisor configured with write-back caching: each of them can acknowledge a flush before the bytes are durable, and Postgres's guarantee is then exactly as good as that acknowledgement. No configuration setting detects it, no log message reports it, and the discovery happens during the one power event the guarantee existed for.
pg_test_fsync is how you find out what the platform actually does. It reports the average time of a file sync operation in microseconds for each available wal_sync_method, running five seconds per test by default, and the finding is usually not the fastest method but a number that is implausibly good for the device class in front of you. The manual notes that the differences it shows may not translate into database throughput, which is fair; the reason to run it is not tuning. It is that a recovery point objective is a promise about this storage, and the tool takes five seconds per method to find out what this storage does.
synchronous_commit = off trades a bounded window of recently committed transactions, at most about 600 milliseconds of them, for throughput on small writes. The database stays consistent and recoverable, and the lost transactions look exactly like transactions that were cleanly aborted.
fsync = off trades the integrity of the cluster itself. A crash can leave data files that the log cannot reconcile, and the result is a data directory nobody can start. There is no partial version of this outcome to plan around.
Where each belongs: the first is a business decision, made per class of data and written down. The second belongs on a benchmark box or a test cluster that is rebuilt from a script, and nowhere that a rebuild would cost anything.
- Setting
fsync = offto fix slow writes after reading thatsynchronous_commit = offis safe — the two are not variants of one parameter, and only one of them can leave you with no cluster. - Relaxing
synchronous_commitglobally when only the ingest path needed it, so the payment writes lose a guarantee that is still written down in the runbook. - Promising a recovery point objective on storage whose flush behaviour has never been measured — a write cache that acknowledges early invalidates every setting above it, and reports nothing.
- Setting
synchronous_standby_namesand then investigating the resulting commit latency as a regression, when it is the network round trip the setting was asking for. - Treating a synchronous replica as the backup — it protects against losing the host and reproduces a mistaken
DELETEwith perfect fidelity. - Leaving a relaxed
synchronous_commitin a session that a pooler will hand to the next caller, instead of scoping it withSET LOCALinside the transaction that wanted it.
- Keep
synchronous_commit = onas the cluster default and relax it withSET LOCALonly on the paths where losing half a second is genuinely acceptable. - Never run production with
fsync = off, and read any advice recommending it as advice about a benchmark rather than about a system holding orders. - Run
pg_test_fsyncon the actual storage before promising a recovery point objective, and treat an implausibly fast result as a finding rather than good news. - Classify every write path by durability class, record the list beside the schema, and revisit it whenever a table's role in the product changes.
- Budget the extra round trip before turning on a remote synchronous level, and measure commit latency on the primary before and after rather than after alone.
innodb_flush_log_at_trx_commit 1/2/0, the same dial renamedOracle commit write batch and nowaitSQL Server delayed durability at database or transaction scopeSQLite PRAGMA synchronous, the miniature of the same argumentKnowledge Check
With the default settings, what has happened by the time COMMIT returns?
- The transaction's WAL records have been flushed to durable storage
- Every data page the transaction modified has been written to its file
- A standby has confirmed that it received and flushed the commit record
- A checkpoint has run to make the transaction's changes permanent
A power failure hits a cluster running with synchronous_commit = off. What is the state afterwards?
- Consistent, minus up to a few hundred milliseconds of committed transactions
- Possibly inconsistent, since pages may disagree with the log on restart
- Missing all transactions written since the most recent checkpoint ran
- Recoverable only from a backup, since local recovery cannot be trusted
Why is fsync = off in a different category from every other setting on this page?
- It gives no measurable speedup, so the risk buys nothing in return
- It can leave data files that the log cannot reconcile after a crash
- It cannot be scoped to one session, unlike the commit-wait setting
- It disables full-page writes, so torn pages are no longer repairable
What does synchronous_commit = remote_apply add over the plain on setting?
- The standby has applied the change and will return it to a reader
- The standby has flushed the commit record to its durable storage
- Every configured standby must answer rather than just the first one
- The primary flushes locally as well as sending the record onward
Why does measuring the platform with pg_test_fsync matter before promising a recovery point?
- Storage that acknowledges a flush early breaks the guarantee undetectably
- The tool computes the recovery window that the current settings can support
- It verifies that archived WAL segments can be replayed at the needed rate
- It sets the fastest sync method automatically once the test has finished
You got correct