MongoDB Transactions & Consistency

Every write to a single MongoDB document is atomic, including updates touching many embedded fields and arrays at once. Because well-designed documents keep related data together, most applications need nothing more. When a change must span multiple documents or collections, such as moving money between accounts or reserving inventory while creating an order, MongoDB supports multi-document ACID transactions across replica sets and sharded clusters.

Transactions are only part of the story. Write concern controls how many replicas must acknowledge a write before it counts as successful. Read concern controls how durable the data you read must be. Read preference decides which members serve reads. Together they set where your application sits between speed and safety.

TL;DR

Quick Example

Transferring credits between two accounts atomically (Node.js driver):

Either all three writes commit or none do. Other clients never see money leave account A without arriving in B.

Core Concepts

Single-Document Atomicity

An updateOne that changes five fields, pushes to an array, and increments a counter on one document either fully applies or doesn't apply at all. Combined with conditional filters ({ balance: { $gte: 100 } }) and operators like $inc, many "transactional" needs are solved with one well-designed document and one update. This is why schema design is the first tool for consistency in MongoDB.

Multi-Document Transactions

Transactions (replica sets since 4.0, sharded clusters since 4.2) give:

Limits and costs: the default maximum runtime is 60 seconds (transactionLifetimeLimitSeconds), large transactions cache a lot of state, and conflicting writes cause write conflicts that abort one transaction. Transactions spanning shards add coordination overhead.

Handling Transient Errors

Transactions can fail with TransientTransactionError (for example a write conflict or failover), which means retry the whole transaction, or UnknownTransactionCommitResult, which means retry the commit. The withTransaction / with_transaction helpers in official drivers implement these retry loops for you. Keep the transaction body free of non-idempotent side effects like sending emails, because it may run more than once.

Write Concern

Read Concern and Read Preference

Read concern decides what data a read may return:

Read preference decides where reads go: primary (default), primaryPreferred, secondary, secondaryPreferred, or nearest. Reading from secondaries spreads load but may return stale data because of replication lag.

Causal Consistency and Retryable Writes

A causally consistent session guarantees read-your-own-writes and monotonic reads even when reading from secondaries: the driver passes cluster times so a secondary waits until it has caught up. Retryable writes (on by default in modern drivers) automatically retry a write once after a network error or failover, with the server deduplicating so the write isn't applied twice.

Best Practices

Prefer Document Design Over Transactions

If the data changed together can live in one document, such as an order and its line items, you get atomicity without transaction overhead. Reach for transactions for genuinely cross-document invariants: transfers, inventory reservations, uniqueness across collections.

Keep Transactions Short

Do reads and computation before starting the transaction when possible, touch few documents, and never wait on user input or slow external calls inside one. Long transactions hold resources and conflict more.

Use majority for Important Writes

The default write concern majority is the right choice for anything you can't afford to lose. Use w: 1 only for high-volume data where occasional loss on failover is acceptable, such as metrics or logs.

Make Operations Idempotent

Retries at the driver and application level mean an operation can execute more than once. Use unique keys, conditional updates, and idempotency keys for externally triggered writes such as payment webhooks.

Common Mistakes

Side Effects Inside a Retried Transaction

The outbox pattern, writing an "email to send" document inside the transaction for a separate worker to process, gives reliable side effects. See the saga pattern for multi-service workflows.

Forgetting to Pass the Session

Every operation that should be part of the transaction must receive { session }. Operations without it run outside the transaction and commit independently, which is a subtle and dangerous bug.

Reading Your Write From a Secondary

Writing to the primary and immediately reading from a secondary can return the old value. Read from the primary for read-your-writes flows, or use causally consistent sessions.

FAQ

Is MongoDB ACID compliant?

Yes. Single-document operations have always been atomic, and multi-document ACID transactions are supported across replica sets and sharded clusters. With write concern majority and snapshot read concern, transactions provide atomicity, consistency, snapshot isolation, and durability.

Are MongoDB transactions slow?

They cost more than single-document writes, with extra round trips, conflict detection, and more work for cross-shard commits, but they're practical for normal use. Performance problems usually come from long-running transactions, hot documents causing write conflicts, or using transactions for everything instead of relying on document atomicity.

What happens to unacknowledged writes during failover?

Writes acknowledged only by the old primary (w: 1) that hadn't replicated may be rolled back when a new primary is elected, and written to rollback files for manual recovery. Writes acknowledged with majority are preserved.

Do I need transactions with a single-node deployment?

Transactions require a replica set, even a single-member one, because they rely on the oplog. Local development setups often run a one-node replica set for this reason.

Related Topics

References