Skip to content
Navigation

Type at least two characters. Search covers page titles, headings, tags and database names.

↑ ↓ to navigateEnter to openEsc to close0 pages
MongoDBadvanced

MongoDB Transactions

Multi-document transactions, their cost and limits, and why the right answer is usually to model around them.

2 min readAdvancedUpdated Edit this page

A single-document write in MongoDB is atomic, including updates to nested fields and arrays. That covers most invariants when the model is designed for it.

Multi-document transactions exist for the rest, on replica sets and sharded clusters.

const session = client.startSession();
try {
  session.startTransaction({
    readConcern: { level: "snapshot" },
    writeConcern: { w: "majority" }
  });
 
  await accounts.updateOne(
    { _id: from, balanceCents: { $gte: amount } },
    { $inc: { balanceCents: -amount } },
    { session }
  );
  await accounts.updateOne({ _id: to }, { $inc: { balanceCents: amount } }, { session });
 
  await session.commitTransaction();
} catch (error) {
  await session.abortTransaction();
  throw error;
} finally {
  await session.endSession();
}

The costs

  • A 60-second default limit (transactionLifetimeLimitSeconds). A transaction exceeding it is aborted.
  • Write conflicts abort the transaction. Two transactions modifying the same document mean one fails with a transient error and must be retried by the application.
  • Cache pressure. WiredTiger keeps a snapshot for the transaction's life, so long transactions hold history in cache.
  • Sharded transactions are considerably more expensive, requiring coordination across shards.

Retrying correctly

Transient failures are normal and must be retried; the driver exposes labels for exactly this:

async function withTransactionRetry(session, fn, maxAttempts = 3) {
  for (let attempt = 1; ; attempt++) {
    try {
      return await fn();
    } catch (error) {
      const transient = error.hasErrorLabel?.("TransientTransactionError");
      if (!transient || attempt >= maxAttempts) throw error;
      await sleep(50 * attempt);   // back off before retrying
    }
  }
}

Drivers also provide withTransaction, which implements the retry loop for both transient errors and unknown commit results. Prefer it over hand-rolled loops.

Prefer modelling to transactions

Where the invariant genuinely spans documents and cannot be localised, use a transaction — but keep it short, touch few documents, and expect to retry.