MongoDB Transactions
Multi-document transactions, their cost and limits, and why the right answer is usually to model around them.
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.