MongoDB guarantees atomicity at the single-document level automatically, but many real-world operations span multiple documents or even multiple collections. Multi-document transactions extend ACID guarantees across these boundaries, while concurrency controls determine how simultaneous operations interact safely. This tutorial covers both â from basic session usage to retryable writes and performance tuning.
Information
đ 1. Introduction
Consider transferring money between two bank accounts: the debit and credit must either both succeed or both fail. Without transactions, a crash between the two writes could leave the data in an inconsistent state. This is exactly the problem multi-document transactions solve.
â 2. What are Transactions?
A transaction groups multiple operations so they execute with ACID guarantees: Atomicity, Consistency, Isolation, and Durability. Either every operation in the transaction succeeds, or none of them take effect.
Note
âī¸ 3. Single Document Atomicity
Every write to a single document â even one that modifies nested arrays or sub-documents â is already atomic in MongoDB, without needing an explicit transaction.
Single-document atomicity requires no transaction
// Atomic without a transaction: both fields update together, or neither does
await collection.updateOne(
{ _id: accountId },
{ $inc: { balance: -100 }, $push: { history: { type: "debit", amount: 100 } } }
);Tip
đ 4. Multi-Document Transactions
When an operation must atomically span multiple documents or collections, wrap it in an explicit transaction using a session.
A basic multi-document transaction
const session = client.startSession();
try {
session.startTransaction();
await accounts.updateOne(
{ _id: fromId },
{ $inc: { balance: -100 } },
{ session }
);
await accounts.updateOne(
{ _id: toId },
{ $inc: { balance: 100 } },
{ session }
);
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}âļī¸ 5. Starting Transactions
A transaction begins with startTransaction() on a session object, optionally configured with specific read and write concerns.
Starting a transaction with explicit concerns
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
});â 6. Committing Transactions
commitTransaction() makes all changes within the transaction permanent and visible to other operations, atomically.
Committing a transaction
await session.commitTransaction();Warning
âšī¸ 7. Aborting Transactions
abortTransaction() discards all operations performed within the transaction, leaving the database exactly as it was before the transaction started.
Aborting on error
try {
// transaction operations
} catch (error) {
await session.abortTransaction();
}đī¸ 8. Session Management
Every transaction runs within a client session, which must be passed explicitly to every operation that's part of the transaction.
Lifecycle of a client session
const session = client.startSession();
// ...use session in every operation...
await session.endSession(); // always release the sessionCaution
đī¸ 9. Read Concerns
Read concern controls the consistency and isolation guarantees of data read within a transaction. The "snapshot" level is most common for transactions, ensuring reads see a consistent point-in-time view.
| Level | Guarantee |
|---|---|
| local | Returns the most recent data without guaranteeing it's been replicated |
| majority | Returns data acknowledged by a majority of replica set members |
| snapshot | Returns a consistent point-in-time view, used within transactions |
âī¸ 10. Write Concerns
Write concern controls how many replica set members must acknowledge a write before it's considered successful.
Setting a write concern
// Require acknowledgment from a majority of replica set members
{ writeConcern: { w: "majority" } }Important
đ 11. Read Preference
Read preference determines which replica set member (primary or secondary) serves read operations. Transactions that include writes must use the primary read preference.
Setting read preference for a transaction
{ readPreference: "primary" }đ§ą 12. Isolation
MongoDB transactions provide snapshot isolation â all reads within the transaction see a consistent view of data as of the transaction's start, unaffected by concurrent writes from other operations.
Note
đĻ 13. Concurrency Control
When multiple operations attempt to modify the same document concurrently, MongoDB must decide how to resolve the conflict â either by locking or by detecting the conflict after the fact.
đ 14. Locking
MongoDB uses fine-grained, document-level locking internally (with intent locks at higher levels), allowing high concurrency across unrelated documents while still preventing conflicting writes to the same one.
Tip
đ¯ 15. Optimistic Concurrency
A common application-level pattern: include a version field in the filter so an update only succeeds if the document hasn't changed since it was read.
Optimistic concurrency with a version field
const doc = await collection.findOne({ _id: docId });
const result = await collection.updateOne(
{ _id: docId, version: doc.version },
{ $set: { status: "shipped" }, $inc: { version: 1 } }
);
if (result.matchedCount === 0) {
console.log("Document changed concurrently â retry needed");
}Tip
đ 16. Retryable Writes
Retryable writes automatically retry a write operation once if it fails due to a transient network error or replica set failover, without risk of duplicate execution.
Enabling retryable writes
const client = new MongoClient(uri, { retryWrites: true });Note
đ 17. Retryable Reads
Similarly, retryable reads automatically retry read operations that fail due to transient network issues, improving resilience during failovers.
Enabling retryable reads
const client = new MongoClient(uri, { retryReads: true });đ 18. Distributed Transactions
On a sharded cluster, a transaction may span documents located on different shards. MongoDB coordinates this transparently using a two-phase commit protocol internally.
Caution
⥠19. Transaction Performance
- Keep transactions short â long-running transactions hold resources and increase conflict likelihood.
- Limit the number of documents modified within a single transaction.
- Avoid transactions entirely when single-document atomicity or optimistic concurrency would suffice.
- Design schemas to minimize the need for cross-document transactions in the first place.
Caution
đ¨ 20. Error Handling
Transaction errors may carry special labels like TransientTransactionError or UnknownTransactionCommitResult, indicating the operation is safe to retry.
Retrying on transient transaction errors
async function runTransactionWithRetry(txnFn, session) {
while (true) {
try {
await txnFn(session);
break;
} catch (error) {
if (error.hasErrorLabel("TransientTransactionError")) {
continue; // retry the whole transaction
}
throw error;
}
}
}Example
đ 21. Best Practices
- Favor schema design (embedding related data) over transactions whenever possible.
- Always pass the session object to every operation within a transaction.
- Keep transactions short-lived and touching as few documents as possible.
- Implement retry logic for TransientTransactionError and UnknownTransactionCommitResult.
- Use optimistic concurrency for high-throughput, low-conflict scenarios instead of full transactions.
â ī¸ 22. Common Mistakes
- Forgetting to pass { session } to an operation, silently excluding it from the transaction.
- Running long, complex logic (like external API calls) inside a transaction, increasing its duration and conflict risk.
- Not handling TransientTransactionError, causing transactions to fail permanently on transient issues.
- Reaching for transactions when good schema design (embedding) would eliminate the need entirely.
- Forgetting to call endSession(), leaking session resources over time.
Best Practice
â 23. Frequently Asked Questions
Yes. Multi-document transactions require MongoDB to be running as a replica set or sharded cluster â they aren't available on a standalone instance.
Generally, yes, due to the coordination overhead involved. Use them only when true multi-document atomicity is required, not as a default for every write.
No. MongoDB does not support nested transactions â a session can only have one active transaction at a time.