🔐 Transactions & Concurrency in MongoDB

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

Examples use the official MongoDB Node.js Driver and assume a replica set or sharded cluster, since transactions require one.

📖 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.

Transaction Lifecycle
Start Session
Start Transaction
Perform Operations
Commit or Abort

❓ 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

MongoDB has supported multi-document transactions on replica sets since version 4.0, and on sharded clusters since 4.2.

âš›ī¸ 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

If your operation only touches one document, you likely don't need a transaction at all — good schema design (favoring embedding) can often avoid the need entirely.

🔀 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

A commit can fail due to write conflicts or network issues — always be prepared to retry the entire transaction on certain error types.

âšī¸ 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 session

Caution

Forgetting to pass the { session } option to an operation means it runs outside the transaction entirely, silently breaking atomicity.

đŸ‘ī¸ 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.

LevelGuarantee
localReturns the most recent data without guaranteeing it's been replicated
majorityReturns data acknowledged by a majority of replica set members
snapshotReturns 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

Transactions typically use w: "majority" to ensure committed changes survive a primary failover.

📖 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

This means two transactions running concurrently won't see each other's uncommitted changes, similar to REPEATABLE READ in relational databases.

đŸšĻ 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.

Concurrency Approaches
Locking (prevent conflicts up front)
Optimistic concurrency (detect conflicts 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

Unlike some databases, you generally don't need to think about explicit locking in MongoDB — its internal concurrency control handles most cases automatically.

đŸŽ¯ 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

Optimistic concurrency avoids the overhead of transactions for cases where conflicts are rare, retrying only when they actually occur.

🔁 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

retryWrites is enabled by default in modern MongoDB drivers when connecting to a replica set or sharded cluster.

🔁 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

Cross-shard transactions carry more overhead than single-shard ones — design shard keys so that related documents commonly land on the same shard when possible.

⚡ 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

MongoDB imposes a default 60-second limit on transaction execution time — transactions that run longer are automatically aborted.

🚨 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

Official MongoDB drivers provide these error labels specifically so applications can implement correct, standardized retry logic.

🌟 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

  1. Forgetting to pass { session } to an operation, silently excluding it from the transaction.
  2. Running long, complex logic (like external API calls) inside a transaction, increasing its duration and conflict risk.
  3. Not handling TransientTransactionError, causing transactions to fail permanently on transient issues.
  4. Reaching for transactions when good schema design (embedding) would eliminate the need entirely.
  5. Forgetting to call endSession(), leaking session resources over time.

Best Practice

Ask whether a schema change could eliminate the need for a transaction before reaching for one — embedding often solves the same problem more simply.

❓ 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.

📌 24. Summary

>>"Use transactions when you truly need them — and design your schema so you rarely do."

Summary

Transactions give MongoDB the full ACID guarantees relational databases are known for, while thoughtful schema design keeps them the exception rather than the rule. Explore data modeling patterns and replica set elections to deepen your understanding of MongoDB's consistency model.