Transactions
MongoDB has supported multi-document ACID transactions since version 4.0 (replica sets) and 4.2 (sharded clusters). Transactions guarantee that a series of operations either all succeed or all fail — essential for operations like financial transfers where partial completion is worse than no completion at all.
ACID in MongoDB
Property | Meaning | MongoDB Support |
|---|---|---|
Atomicity | All operations succeed or all are rolled back | Single-doc always; multi-doc with transactions |
Consistency | Data remains valid after the transaction | Yes — enforced by schema validation and app logic |
Isolation | Concurrent transactions do not interfere | Snapshot isolation within a transaction |
Durability | Committed data survives crashes | Yes — with write concern majority |
When to Use Transactions
Bank transfers — debit one account and credit another atomically
Order placement — create the order and decrement inventory simultaneously
Payment processing — record the payment and activate the subscription together
Any operation that must fully succeed or fully fail across multiple collections
Single-Document Atomicity
Operations on a single document are always atomic in MongoDB — no transaction is needed.
Updates that use $set, $inc, $push, and similar operators are applied atomically
to one document. Use subdocuments to keep tightly related data together so that a single
atomic write covers the whole update.
Transaction Syntax in Node.js
const session = client.startSession()
try {
await session.withTransaction(async () => {
await accounts.updateOne(
{ _id: fromId },
{ $inc: { balance: -amount } },
{ session }
)
await accounts.updateOne(
{ _id: toId },
{ $inc: { balance: amount } },
{ session }
)
})
} finally {
await session.endSession()
}Transaction in mongosh
// Start a session
const session = db.getMongo().startSession()
// Begin the transaction
session.startTransaction()
try {
const accounts = session.getDatabase('bank').getCollection('accounts')
accounts.updateOne(
{ _id: "account-A" },
{ $inc: { balance: -100 } }
)
accounts.updateOne(
{ _id: "account-B" },
{ $inc: { balance: 100 } }
)
// Commit — both updates become visible atomically
session.commitTransaction()
print("Transaction committed")
} catch (err) {
// Roll back — neither update is applied
session.abortTransaction()
print("Transaction aborted:", err.message)
} finally {
session.endSession()
}Error Handling and Retry
Transactions can fail with two types of errors:
- TransientTransactionError — a temporary condition (network glitch, write conflict). Retry the entire transaction from the beginning.
- UnknownTransactionCommitResult — the commit's outcome is unknown (timeout on the acknowledgment). Retry only the commit — the operations themselves already ran.
Always wrap transaction logic in retry handlers for production code.
async function runWithRetry(txnFn, client) {
const session = client.startSession()
try {
let committed = false
while (!committed) {
try {
await session.withTransaction(txnFn)
committed = true
} catch (err) {
if (err.hasErrorLabel('TransientTransactionError')) {
// Retry the whole transaction
console.log('TransientTransactionError — retrying...')
continue
}
if (err.hasErrorLabel('UnknownTransactionCommitResult')) {
// Retry only the commit
console.log('UnknownTransactionCommitResult — retrying commit...')
continue
}
throw err
}
}
} finally {
await session.endSession()
}
}Performance Considerations
Transactions carry overhead compared to single-document operations
Keep transactions short — the default timeout is 60 seconds
Minimize the number of operations and collections touched per transaction
Documents are locked during a transaction; concurrent writers to the same documents will contend
Cross-shard transactions (sharded clusters) use a two-phase commit and are more expensive