What it means
Inside a transaction, the first write to a document holds on to it until the transaction commits
or aborts. When a second transaction tries to change the same document in that time, MongoDB doesn’t
make it wait: it fails the write at once with code 112, WriteConflict, and aborts that
transaction. The error carries the label TransientTransactionError, which means “retrying the
whole transaction may work”.
The message has varied between versions; MongoDB 8.0 says:
Caused by :: Write conflict during plan execution and yielding is disabled. :: Please retry your operation or multi-document transaction.
Older versions said WriteConflict error: this operation conflicted with another operation. Please retry your operation or multi-document transaction. After the conflict the transaction is gone, so
further commands in it fail with code 251, NoSuchTransaction:
Transaction with { txnNumber: 1 } has been aborted.
A transaction also gets this error when it writes a document that a write outside any transaction changed after the transaction began. Writes outside transactions never get it themselves: one that reaches a document a transaction has already changed waits until the transaction ends (or until its own time limit runs out).
Common causes
- Two requests change the same document at once, each in a transaction: a counter, a balance, a stock level, a shared settings document.
- Long transactions, which hold their documents longer and collide more often: slow work, an external API call or user input in the middle of one.
- A hot document that every transaction touches, such as a global sequence or totals document.
- Ordinary writes to documents a transaction is about to change: a background job updating documents while transactions read and then write them.
- Hand-written transactions without a retry loop, using
startTransaction()andcommitTransaction()directly. The conflict is normal; the missing retry is the bug.
How to fix it
Retry the whole transaction
The drivers’ callback API retries for you. In Node.js, withTransaction reruns the callback when
the error has the TransientTransactionError label, and retries the commit when it’s unknown
whether it succeeded:
const session = client.startSession()
try {
await session.withTransaction(async () => {
await accounts.updateOne({ _id: "alice" }, { $inc: { balance: -10 } }, { session })
await accounts.updateOne({ _id: "bob" }, { $inc: { balance: 10 } }, { session })
})
} finally {
await session.endSession()
}
The callback may run more than once, so keep side effects (emails, HTTP calls) outside it. If you
manage transactions yourself, catch the error, check
err.hasErrorLabel("TransientTransactionError"), abort and start again from the beginning.
Keep transactions short
Do slow work (API calls, computing, waiting for input) before the transaction starts, and keep inside it only the reads and writes that must happen together. The shorter it is, the smaller the window for a conflict.
Spread out hot documents
Split a single counter into several (one per worker, or one per hour) and add them up when you read, or record events as new documents instead of updating one.
Use a single-document write when it’s enough
One update to one document is atomic without a transaction. $inc: { stock: -1 } with a filter of
{ stock: { $gte: 1 } } decrements safely under concurrency, and writes outside transactions
wait instead of failing.
Reproduce it
A temporary single-member replica set of our own (mongo:8.0 --replSet rs0, MongoDB 8.0.32, removed
afterwards), since transactions need one. Two sessions in mongosh 2.12.0:
const s1 = db.getMongo().startSession()
const s2 = db.getMongo().startSession()
const a1 = s1.getDatabase("seo_err_mongo").accounts
const a2 = s2.getDatabase("seo_err_mongo").accounts
s1.startTransaction()
a1.updateOne({ _id: "alice" }, { $inc: { balance: -10 } })
s2.startTransaction()
a2.updateOne({ _id: "alice" }, { $inc: { balance: -20 } })
MongoServerError[WriteConflict]: Caused by :: Write conflict during plan execution and yielding is disabled. :: Please retry your operation or multi-document transaction.
The error’s code was 112 and its errorLabels [ 'TransientTransactionError' ]. Repeating the
write in the same session gave code 251, NoSuchTransaction. After s1 committed, Alice had 90:
only the first transaction’s change was kept.
A plain updateOne on the same document from another connection, while the first transaction was
still open, didn’t conflict: it waited, and with { maxTimeMS: 2000 } it failed after 2 seconds
with operation exceeded time limit. The other order did conflict: a transaction read Bob’s
document, a plain update outside it added 100 to his balance, and the transaction’s own update of
Bob then failed with the same code 112 and label.
With the Node.js driver 6.20.0, two withTransaction calls changed the same document, the first
holding its transaction for half a second. The second callback ran 83 times, retried by the driver,
and succeeded once the first committed; both increments were kept.
In Inlet
Inlet runs MongoDB queries in mongosh syntax, so you can look at the documents your transactions wrote, as tables and in the inspector. Edits made in Inlet are checked against what you loaded: if someone changed the document meanwhile, nothing is saved and Inlet shows their version beside yours.