Transaction Retries

Transaction Limits and Retry Patterns

Two transactions that touch the same document do not queue: the second fails at once with WriteConflict, code 112, labeled TransientTransactionError. One that runs past transactionLifetimeLimitSeconds is aborted by the server, and its next operation fails with NoSuchTransaction, code 251, also transient.

A write conflict and an expired transaction, with their real errors
s1.startTransaction(); s2.startTransaction();          // two clients, same document
await ca.updateOne({ _id: 'A' }, { $inc: { bal: 1 } }, { session: s1 });
try { await cb.updateOne({ _id: 'A' }, { $inc: { bal: 2 } }, { session: s2 }); }
catch (e) { console.log(e.code, e.codeName, JSON.stringify(e.errorLabels),
  e.hasErrorLabel('TransientTransactionError')); }
await admin.command({ setParameter: 1, transactionLifetimeLimitSeconds: 3 });
const s = a.startSession(); s.startTransaction();
await c.insertOne({ step: 1 }, { session: s });
await new Promise(r => setTimeout(r, 6000));           // longer than the limit
try { await c.insertOne({ step: 2 }, { session: s }); }
catch (e) { console.log(e.code, e.codeName, e.message.slice(0, 44)); }
console.log('docs left:', await c.countDocuments());
Output
112 WriteConflict ["TransientTransactionError"] true
251 NoSuchTransaction Transaction with { txnNumber: 1 } has b
docs left: 0

The expired transaction left nothing behind. Both errors are transient, so the answer is to retry the whole transaction, which withTransaction does: it retries on TransientTransactionError and retries the commit on UnknownTransactionCommitResult, the label hand-rolled loops miss. Your part is an idempotent callback.

Transaction limits in MongoDB 8.3 1,815 , read back with getParameter
Limit Default and parameter
Transaction runtime 60 s, transactionLifetimeLimitSeconds
Lock acquisition wait 5 ms, maxTransactionLockRequestTimeoutMillis
Size of one oplog entry 16 MB, fixed by the BSON limit
Total transaction size WiredTiger cache, TransactionTooLargeForCache

Since MongoDB 4.2 a transaction writes one oplog entry per operation rather than one for everything, so the old 16 MB ceiling on a whole transaction is gone; each entry still must fit in 16 MB. The real ceiling is the WiredTiger cache, which holds every uncommitted change until commit. The cost is measurable: 2,000 documents inserted one per transaction took 30,178 ms against 939 ms as plain inserts, and 786 ms inside one transaction. The commit dominates, so batch related work together and never wrap a single write in one.