Dirty Reads, Explained
A dirty read returns an uncommitted value from another transaction. If that transaction rolls back, the reader observed a value that was never part of committed database state.
Account 42 starts with a committed balance of $500. Transaction A is a payment service attempting to debit the account, so it updates the balance to $0 while it validates the payment. Transaction B is the account dashboard, which reads the balance to display it to the customer. If Transaction A later rolls back, the balance returns to $500, but Transaction B has already displayed $0.
That is a dirty read: one transaction reads another transaction's uncommitted changes.
The initial state
Before either transaction starts, the database contains one committed row:
| Account ID | Balance |
|---|---|
| 42 | $500 |
Transaction A owns the payment attempt. Its update is not committed because payment validation is still in progress. Transaction B only wants to display the current balance. It is configured with READ UNCOMMITTED, so it can read Transaction A's unfinished update.
The two-transaction problem
Transactions normally prevent one session from reading another session's unfinished changes. A dirty read allows that access, which can reduce waiting but exposes an intermediate database state.
The dashboard displayed 0, even though the committed balance remained $500 throughout. If Transaction B uses the value to approve another withdrawal, send an email, or populate a cache, a downstream operation can use data from a transaction that later rolled back.
A small SQL example
MySQL supports the READ UNCOMMITTED isolation level, which permits this behavior:
-- Initial committed state: account 42 has a balance of 500.
-- Session A: payment service starts a debit.
START TRANSACTION;
UPDATE accounts SET balance = 0 WHERE id = 42;
-- Payment validation is still in progress. The update is uncommitted.
-- Session B: account dashboard reads the balance while Session A is open.
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 42;
-- Result: 0
-- Session A: payment validation fails, so the debit is cancelled.
ROLLBACK;
-- The committed balance remains 500.The exact syntax and behavior vary by database. InnoDB permits dirty reads at READ UNCOMMITTED. PostgreSQL accepts the label but maps it to READ COMMITTED, so this example does not produce a dirty read there. Oracle does not support READ UNCOMMITTED. Always verify the isolation semantics of the database engine and driver in use instead of assuming that a standard isolation-level name has identical behavior everywhere.
Why anyone enables it
The trade-off is simple: potentially less waiting in exchange for weaker correctness. A reader at READ UNCOMMITTED may avoid waiting for a writer's commit, but this is an implementation detail, not a guarantee that the query never blocks. The setting can look attractive for a low-value report or a heavily contended table.
The cost is not just an incorrect number. The reader can observe an intermediate state, read a row that later disappears, or make a decision before the writer commits. A retry can return a different answer immediately afterward, making the problem difficult to reproduce.
When not to use it
Do not use dirty reads for money, inventory, permissions, quotas, job state, or any decision that triggers an external side effect. A faster query is not useful if it causes a payment, reservation, or notification based on data that gets rolled back.
For most application reads, start with the database engine's documented default isolation level. Defaults differ: PostgreSQL defaults to READ COMMITTED, while InnoDB defaults to REPEATABLE READ. If a report needs a stable view without blocking writers, investigate snapshot-based isolation or a replica, and measure the freshness, version-retention, and operational costs.
Gotchas and alternatives
The common mistake is treating READ UNCOMMITTED as a harmless performance switch. It changes which database states the application can observe. In multiversion concurrency control (MVCC) engines, ordinary snapshot reads and locking reads can also have different behavior, so the isolation level alone is not the whole concurrency model. Connection pools create an additional configuration risk: isolation settings can persist on a reused connection, so set and reset them deliberately.
The main alternatives are:
READ COMMITTED: read only committed data, but separate statements can see different committed versions.REPEATABLE READ: commonly keep a consistent snapshot for ordinary reads in a transaction, but details such as locking reads and write conflicts vary by engine.SERIALIZABLE: provide the strongest isolation, at the cost of more waits, aborts, or transaction retries.- A read replica or precomputed report: move analytical work away from the primary, accepting replication lag.
References
- PostgreSQL 16 Documentation, Transaction Isolation postgresql.org
- MySQL 8.4 Reference Manual, InnoDB Transaction Isolation Levels dev.mysql.com
Takeaway
A dirty read exposes an uncommitted value, so use it only when the application can tolerate results that may be rolled back before they become committed state.