The remaining two of the three default_transaction_* parameters, completing the set default_transaction_deferrable began. Each sets the session default for one of the three properties a transaction carries — isolation level, access mode, deferrability — and for all three the rule is the same: the GUC scope is rarely the one you want. The interesting content is what the values mean, which for isolation is more than most people think.

default_transaction_isolation

Default read committed, context user. It sets the isolation level new transactions get, and the values are where the surprise lives: PostgreSQL accepts all four SQL-standard levels — read uncommitted, read committed, repeatable read, serializable — but implements only three. read uncommitted is silently treated as read committed, because PostgreSQL’s MVCC never exposes uncommitted data to anyone under any setting; there are no dirty reads to permit, so the docs call mapping the two together “the only sensible way to map the standard isolation levels to PostgreSQL’s multiversion concurrency control architecture.” You can ask for read uncommitted; you will get read committed; this is not a bug.

The three real levels differ in what they freeze and when. Read committed takes a fresh snapshot at the start of each statement — so two SELECTs in one transaction can see different data if something committed in between. It prevents dirty reads and nothing else. Repeatable read takes one snapshot at the start of the transaction’s first statement and holds it; every query sees that same frozen moment. PostgreSQL implements it with snapshot isolation, and a quirk worth knowing is that its repeatable read is stronger than the standard requires — it forbids phantom reads, which the SQL standard permits at this level. The cost is that a write conflict against concurrent activity surfaces as a serialization failure (40001) that the transaction must catch and retry. Serializable adds SSI on top, catching the higher-order anomalies repeatable read misses, at the price of the dependency-tracking machinery the deferrable post described — and the same 40001 retry obligation.

That retry obligation is why raising this globally is a decision, not a tweak. Moving the cluster default to repeatable read or serializable changes application semantics: any transaction can now fail with a serialization error it never saw before, and code that doesn’t catch 40001 and retry will surface those as user-facing errors. The levels above read committed are opt-in for code that was written to expect them. The right pattern is to leave the global default at read committed and raise the level for the specific transactions — or the specific roles — whose correctness needs it, in a codebase that retries.

default_transaction_read_only

Default off, context user. When on, new transactions start in read-only mode, which forbids anything that would change the database: INSERT, UPDATE, DELETE, MERGE, COPY FROM into a non-temporary table, and the DDL and maintenance commands that write. The notable carve-out is temporary tables — a read-only transaction can still write to them, because temp tables aren’t the database state the mode is protecting. It’s a guardrail, not a sandbox: a way to declare intent and have PostgreSQL enforce it, so a reporting job that should never write can’t, even by accident.

Two things make it more than a nicety. On a hot standby, transactions are read-only no matter what this is set to — the replica physically cannot write, so the setting is moot there; it matters on a primary, where writing is possible and you want to forbid it for some connections. And it’s the access-mode third of the reporting-role recipe: a login dedicated to analytics or backups against a serializable workload wants all three properties at once, which is exactly where these GUCs finally earn a non-default value:

1ALTER ROLE reporting SET default_transaction_isolation = 'serializable';
2ALTER ROLE reporting SET default_transaction_read_only = on;
3ALTER ROLE reporting SET default_transaction_deferrable = on;

That role now opens every transaction serializable, read-only, and deferrable — it waits for a safe snapshot, runs without SSI overhead, never suffers or causes a serialization failure, and cannot write. For that specific shape of work it’s exactly right, and it’s the one case across all three parameters where setting the default rather than the per-transaction keyword is the better tool.

Everywhere else, leave both at their defaults. read committed and read-write are correct for general workloads, and the transactions that need otherwise are better served by BEGIN TRANSACTION ISOLATION LEVEL ... READ ONLY at the point of use — explicit, local, and impossible to forget you set three connections ago.