MongoDBTransactions

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

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

JS
// 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.

JS
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

Warning
Wrapping every operation in a transaction drastically reduces throughput. Use transactions only when you genuinely need multi-document atomicity. For most CRUD work, a single well-designed document is sufficient.
Note
Well-designed embedded documents eliminate most transaction needs. If an order and its line items live in one document, updating both is a single atomic operation — no transaction required. Transactions are a safety net, not the default tool.
Tip
Use withTransaction() from the MongoDB driver — it handles retry logic for both TransientTransactionError and UnknownTransactionCommitResult automatically.