-
Type:
Bug
-
Resolution: Unresolved
-
Priority:
Major - P3
-
None
-
Affects Version/s: None
-
Component/s: None
-
None
-
Storage Execution
-
ALL
-
Storage Execution 2026-09-28, Repl 2026-08-03, Repl 2026-08-17, Repl 2026-08-31, Repl 2026-09-14
-
None
-
None
-
None
-
None
-
None
-
None
-
None
Problem
Every field under latencyStats.transactions in $collStats output (ops, latency, histogram, queryableEncryptionLatencyMicros) is permanently zero, for every collection, regardless of transaction activity. No code path in the server increments the per-collection transactions bucket — the field exists in the output only because the per-collection histograms share the four-bucket OperationLatencyHistogram type with the server-wide histogram, and only the server-wide instance is ever fed.
The documentation (collStats) describes the field as "Latency statistics for database transactions" with no caveat, and its position alongside the populated reads/writes/commands buckets implies per-collection tracking. This is misleading: there is currently no input whatsoever that produces a non-zero value.
Where the behavior comes from (v8.0 branch; same on master)
- Statements executed inside a multi-document transaction keep their command's static classification — CurOp::getReadWriteType() (src/mongo/db/curop.cpp) returns Command::getReadWriteType(), with no in-transaction override — so per-collection histograms count them under reads/writes.
- Transaction latency is recorded only at commit/abort, and only globally: TransactionMetricsObserver::onCommit/_onAbort (src/mongo/db/transaction/transaction_metrics_observer.cpp) → Top::incrementGlobalTransactionLatencyStats (src/mongo/db/stats/top.cpp), which feeds _globalHistogramStats — surfaced as serverStatus().opLatencies.transactions. There is no per-namespace equivalent. (On master the same shape exists as ServiceLatencyTracker::incrementForTransaction.)
Repro (verified on 8.0.27-ent, sharded cluster; also applies to replica sets)
const coll = db.getSiblingDB("txntest").getCollection("orders"); coll.drop(); coll.insertOne(\{_id: "seed"}); // create collection outside the txn const session = db.getMongo().startSession(); const sColl = session.getDatabase("txntest").getCollection("orders"); for (let i = 0; i < 10; i++) { session.withTransaction(() => { for (let j = 0; j < 4; j++) sColl.insertOne(\{ txn: i, op: j }); sColl.findOne(\{ txn: i }); }); } session.endSession(); coll.aggregate([\{ $collStats: { latencyStats: {} } }]).forEach(d => printjson(\{ host: d.host, latencyStats: d.latencyStats }));
Actual: the 40 transactional inserts appear in writes.ops and the 10 transactional finds in reads.ops, while transactions stays {{{ latency: 0, ops: 0 }}}. Meanwhile serverStatus().opLatencies.transactions.ops on the mongod that executed the transactions increases by 10 — confirming the transactions ran and were counted, just never at collection level.
Expected: latencyStats.transactions reflects transaction activity on the collection — or is not emitted at all.
Proposal
Either:
- Remove/suppress the field in per-collection $collStats output, since it promises data it can never contain; or (preferred)
- Define and populate it:
- transactions.ops = number of transactions that touched this collection (increment once per affected namespace at commit — the participant already tracks a transaction's affectedNamespaces);
- optionally transactions.reads / transactions.writes = number of statements executed within transactions against this collection. This is well-defined and cheap: the server already classifies every such statement per namespace on the same code path that increments reads/writes today.
Note the historically thorny part is only latency attribution (charging a whole transaction's duration to each touched collection is ambiguous); the counts above have unambiguous semantics, so option 2 can populate ops (and the optional statement counters) while leaving per-collection transaction latency at zero or defined as the sum of in-transaction statement latencies.
Motivation
We were trying to build collection-level transaction-activity detection on this field. The always-zero field passed design review precisely because it exists and is documented. Today no per-collection transaction signal exists anywhere in the server — the nearest substitutes (serverStatus.opLatencies.transactions, serverStatus.transactions.*) are node-scoped only, not collection-level.