MongoDB Cheatsheet

Transactions

Use this MongoDB reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Requirements

Multi-document transactions need a replica set (4.0+) or a sharded cluster (4.2+). A standalone mongod cannot run one at all, and there is no flag that changes that.

MongoServerError: Transaction numbers are only allowed on a replica set
member or mongos
DeploymentTransactions
Standalone mongodNo. Not configurable
Single-node replica setYes. This is how you develop locally
Multi-node replica setYes
Sharded clusterYes, 4.2+, with more overhead across shards
Atlas (any tier)Yes, every cluster is a replica set

Converting a local standalone into a one-node replica set is a two-minute job:

mongod --replSet rs0 --dbpath /data/db --bind_ip 127.0.0.1
rs.initiate()                    // one member, defaults are fine for dev
rs.status().myState              // 1 means PRIMARY

withTransaction, the pattern to use

session.withTransaction(callback) is the correct API. It starts the transaction, commits it, and retries on transient failure. Hand-rolled commit logic almost never handles the retry cases, which is the whole reason this method exists.

const session = db.getMongo().startSession({ readPreference: { mode: "primary" } });

try {
  session.withTransaction(async () => {
    const accounts = session.getDatabase("bank").getCollection("balances");
    const ledger  = session.getDatabase("bank").getCollection("ledger");

    const from = accounts.findOne({ _id: "pat", balance: { $gte: 100 } });
    if (from === null) throw new Error("insufficient funds");

    accounts.updateOne({ _id: "pat"   }, { $inc: { balance: -100 } });
    accounts.updateOne({ _id: "sasha" }, { $inc: { balance:  100 } });
    ledger.insertOne({ from: "pat", to: "sasha", amount: 100, at: new Date() });
  }, {
    readConcern:  { level: "snapshot" },
    writeConcern: { w: "majority" },
    maxCommitTimeMS: 5000
  });
} finally {
  session.endSession();
}

Two things make or break this:

- Every operation must run on the session. In mongosh that means going through session.getDatabase(...), not the global db. An operation on plain db inside the callback silently executes outside the transaction and is not rolled back. - The callback must be idempotent and side-effect free. It can run more than once. Do not send an email, charge a card, or mutate a JS variable the caller reads.

Throwing from the callback aborts the transaction and the error propagates. That is how you roll back deliberately.

In a driver the same pattern applies, and the session is an explicit argument:

// Node driver
const session = client.startSession();
try {
  await session.withTransaction(async () => {
    await accounts.updateOne({ _id: "pat" }, { $inc: { balance: -100 } }, { session });
    await ledger.insertOne({ amount: 100 }, { session });
  }, { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } });
} finally {
  await session.endSession();
}

Forgetting { session } on one call is the single most common transaction bug in driver code, and it fails silently.

Manual control

The explicit form exists and you should reach for it only when you genuinely need to interleave logic between the start and the commit.

const session = db.getMongo().startSession();
session.startTransaction({
  readConcern:  { level: "snapshot" },
  writeConcern: { w: "majority" }
});
try {
  const coll = session.getDatabase("bank").balances;
  coll.updateOne({ _id: "pat" },   { $inc: { balance: -100 } });
  coll.updateOne({ _id: "sasha" }, { $inc: { balance:  100 } });
  session.commitTransaction();
} catch (e) {
  session.abortTransaction();
  throw e;
} finally {
  session.endSession();
}

This version has no retry. A write conflict from a concurrent update, or a primary step-down mid-commit, surfaces as an error your caller has to handle. If you write this, you are re-implementing withTransaction and you have to implement both retry loops described below.

Read and write concerns

SettingValueMeaning
readConcern"snapshot"Every read in the transaction sees one consistent point in time. The right default
readConcern"local"Default. Latest local data, no snapshot guarantee across reads
readConcern"majority"Data acknowledged by a majority
writeConcern{ w: "majority" }Commit acknowledged by a majority. Required for durability across a failover
writeConcern{ w: 1 }Primary only. A failover can lose the committed transaction
readPreferenceprimaryThe only supported value. Transactions never read from a secondary
maxCommitTimeMSmsCap the commit itself

Concerns are set on the transaction, not on the individual operations, and an operation-level writeConcern inside a transaction is an error. Snapshot reads plus majority writes is the combination that actually gives you the isolation people assume they already have.

Error labels

MongoDB tags retryable failures with labels rather than making you match error codes. withTransaction reads exactly these two.

LabelMeansCorrect response
TransientTransactionErrorThe whole transaction failed cleanly and nothing was applied. Covers WriteConflict, LockTimeout, NoSuchTransaction, and a step-down before commitRetry the entire transaction from the start
UnknownTransactionCommitResultThe commit was sent and the outcome is unknown. It may already have succeededRetry the commit only, which is safe because commit is idempotent
// What withTransaction does for you
while (true) {
  try { /* run body, commit */ break }
  catch (e) {
    if (e.hasErrorLabel("TransientTransactionError")) continue;      // full retry
    if (e.hasErrorLabel("UnknownTransactionCommitResult")) continue; // commit retry
    throw e;
  }
}

WriteConflict is normal under concurrency, not a bug. MongoDB uses optimistic concurrency, so two transactions touching the same document mean one of them aborts and retries. Design for it by keeping transactions short.

Limits and costs

LimitValue
transactionLifetimeLimitSeconds60 seconds by default. A periodic task aborts anything older
Individual oplog entry16MB. A large transaction is split across entries, but no single entry may exceed it
Idle transactionHolds a snapshot, so it pins WiredTiger cache and blocks oplog truncation
maxTimeMSApplies per operation, not to the transaction as a whole
Capped collectionsCannot be written to inside a transaction
count / estimatedDocumentCountNot supported. countDocuments is
$out / $mergeNot supported inside a transaction
Collection and index creationAllowed since 4.4, but not implicitly across shards
Sharded transactionsExtra coordination per shard. Cost grows with the number of shards touched

A long-running transaction is the failure mode nobody plans for. It pins a snapshot, grows cache pressure, prevents oplog truncation, and then gets killed at 60 seconds with everything rolled back. Batch large work into many small transactions instead of raising the limit.

When you do not need one

MongoDB's atomicity guarantee is per document, and that is a stronger tool than it sounds. A single-document update is always atomic, however deeply nested the fields and however many operators you apply.

// Atomic without a transaction, because it is one document
db.orders.updateOne({ _id: id, status: "pending" }, {
  $set:  { status: "paid", paid_at: new Date() },
  $inc:  { version: 1 },
  $push: { events: { kind: "payment", at: new Date() } }
})
Instead of a transactionDo this
Order plus its line itemsEmbed the items. One document, one atomic write
Read then conditionally writePut the condition in the filter, or use findOneAndUpdate
Optimistic lockingMatch on version in the filter and $inc it. Zero matches means somebody else won
A counter shared across documents$inc on one counter document
A long multi-step workflowPersist a state field and drive it forward idempotently

Transactions are the right answer for a genuine cross-document invariant, such as double-entry bookkeeping or moving an item between two collections. They are the wrong answer for a schema that was drawn as relational tables and then stored as MongoDB collections. If you find yourself needing transactions on every write path, the document model is what needs changing.