Three kinds of long-running work
When a MongoDB server slows down, the cause is often one operation that has been running far longer than it should. It is usually one of three things:
- An active operation that is still working: a collection scan, a large update, an aggregation without a useful index.
- An idle transaction: a multi-document transaction that started, did some work and is now waiting on the client. It does nothing, but it keeps its locks and holds back the storage cache.
- An idle cursor: an open cursor that the client stopped reading.
All three appear in the $currentOp aggregation stage. The examples use mongosh against MongoDB 8.0; most work on 5.0 and later, and steps that need a newer version say so. MongoDB recommends $currentOp over the currentOp command, which is deprecated since 6.2, and over db.currentOp().
1. Find active operations
db.getSiblingDB("admin").aggregate([
{ $currentOp: { allUsers: true } },
{ $match: { active: true, secs_running: { $gte: 30 }, client: { $exists: true } } },
{ $project: { _id: 0, opid: 1, secs_running: 1, op: 1, ns: 1,
appName: 1, client: 1, effectiveUsers: 1,
planSummary: 1, waitingForLock: 1, killPending: 1 } },
{ $sort: { secs_running: -1 } }
])- Run it on
admin. A pipeline that starts with$currentOpcan only run on theadmindatabase, and$currentOpmust be the first stage. allUsers: trueshows every user's operations, not only yours. With access control on, it needs theinprogprivilege.client: { $exists: true }keeps operations that came from a client connection and leaves out most background work.clientis reported on standalones and replica sets; onmongossee step 6.secs_runningis only present whileactiveistrue. Usemicrosecs_runningfor finer resolution.
2. Find idle transactions
An idle transaction has no running operation, so it has no secs_running. It appears with type: "idleSession" and a transaction document that tells you how long it has been open.
db.getSiblingDB("admin").aggregate([
{ $currentOp: { allUsers: true, idleSessions: true } },
{ $match: { "transaction.timeOpenMicros": { $exists: true } } },
{ $project: { _id: 0, type: 1, opid: 1, appName: 1, client: 1, lsid: 1,
openSecs: { $divide: ["$transaction.timeOpenMicros", 1000000] },
idleSecs: { $divide: ["$transaction.timeInactiveMicros", 1000000] },
expiryTime: "$transaction.expiryTime" } },
{ $sort: { openSecs: -1 } }
])idleSessionsdefaults totrue; it is set here so the intent is clear.timeOpenMicrosis the transaction's age. It equalstimeActiveMicrosplustimeInactiveMicros, so a largeidleSecsmeans the client is holding the transaction open without sending work.expiryTimeis when MongoDB will abort the transaction by itself: its start time plustransactionLifetimeLimitSeconds, which defaults to 60 seconds. A transaction much older than a minute means someone raised that limit.lsidis the session id. You need it in step 5.
For idle cursors, run $currentOp with idleCursors: true and match type: "idleCursor"; the cursor field shows its namespace and when it was last used.
3. Read what it is doing
Before you stop anything, look at the whole document for that operation, not only the summary columns.
db.getSiblingDB("admin").aggregate([
{ $currentOp: { allUsers: true } },
{ $match: { opid: 48213 } }
])command: the full command, including the filter, pipeline or update. This is what you will fix later.planSummary:COLLSCANmeans it is reading every document;IXSCAN { … }names the index.appName,clientandeffectiveUsers: which program, host and database user started it. If these are empty or point at an internal user, leave the operation alone.waitingForLock:truemeans it is waiting for another operation, so the long-running one may not be this one.lockStats.*.timeAcquiringMicrosshows how long it has waited in total.waitingForFlowControl:trueon a replica set primary means writes are being slowed because secondaries are behind, not because of this query.desc: a description of the thread or connection, useful for telling client connections from background work.
command field holds the real filter values and update documents. Treat the output like query results: do not paste it into tickets or chat without removing personal data.4. Stop an operation with killOp
db.killOp(48213)appName, client and effectiveUsers first, and tell the team that owns the program.- It is not instant.
killOpflags the operation, andkillPendingturnstrue. The operation ends at its next safe termination point. Run the query from step 1 again to confirm it is gone. - Privileges. On a
mongodyou can always kill your own operations. Killing another user's operation needs thekillopprivilege. On MongoDB Atlas it is limited to the database user who ran the operation. - The client will retry. The application receives an error. If it retries automatically, the same operation can be back within seconds, so fix the cause or stop the job as well.
5. End a session with killSessions
An idle transaction has no operation for killOp to stop. End its session instead, using the lsid from step 2. Killing a session kills any in-progress operations in it and closes their cursors.
const tx = db.getSiblingDB("admin").aggregate([
{ $currentOp: { allUsers: true, idleSessions: true } },
{ $match: { "transaction.timeOpenMicros": { $exists: true } } },
{ $sort: { "transaction.timeOpenMicros": -1 } },
{ $limit: 1 }
]).toArray()[0];
printjson({ lsid: tx.lsid, appName: tx.appName, client: tx.client });
// only after checking the output above
db.adminCommand({ killSessions: [ tx.lsid ] })killSessions: [] kills all sessions, or with access control on, all sessions owned by your user. Always pass the specific lsid. With access control on, killSessions only ends sessions owned by your user unless your role allows otherwise, and it ignores transactions in the prepared state (writes across shards whose commit is already in progress).Run the query from step 2 again. If the transaction is still listed, check its expiryTime: MongoDB's periodic cleanup aborts it once transactionLifetimeLimitSeconds has passed.
7. Set limits so it stops itself
Killing operations by hand is the last step. Time limits mean most of them end on their own.
db.orders.find({ status: "PENDING" }).maxTimeMS(5000)
db.orders.aggregate(
[ { $group: { _id: "$customerId", total: { $sum: "$amount" } } } ],
{ maxTimeMS: 30000 }
)When the limit is reached, MongoDB stops the operation and returns a MaxTimeMSExpired error. Drivers expose the same option on each operation.
db.adminCommand({
setClusterParameter: { defaultMaxTimeMS: { readOperations: 30000 } }
})
db.adminCommand({ getClusterParameter: "defaultMaxTimeMS" })- Reads only. It applies to read operations such as
find,count,distinctandaggregate(except pipelines with$mergeor$out). The default is0, meaning no limit. - A query's own
maxTimeMSwins. Reports and analytics jobs that legitimately run longer must set their ownmaxTimeMS, or the default will stop them.
db.adminCommand({ getParameter: 1, transactionLifetimeLimitSeconds: 1 })
// back to the default of 60 seconds
db.adminCommand({ setParameter: 1, transactionLifetimeLimitSeconds: 60 })defaultMaxTimeMS or transactionLifetimeLimitSeconds will make some existing jobs fail. Find the longest legitimate reads and transactions first with the queries in steps 1 and 2. setParameter lasts until restart, so also set it in the configuration file, and in a sharded cluster set it on every shard replica set member. On MongoDB Atlas, set the transaction lifetime through Atlas rather than setParameter.The same steps in Quorum
In Quorum you run these commands in Query Studio against your MongoDB connection, with the target and environment printed beside Run, so you know which server a killOp goes to before you send it.
| opid | appName · ns | running | plan · lock |
|---|---|---|---|
| 48213 | nightly-export · shop.orders | 14 min 06 s | COLLSCAN |
| 48877 | orders-api · shop.orders | 41 s | IXSCAN · waiting for lock |
| idle txn | billing-sync · shop.invoices | open 52 s | idle 49 s |
- Query Studio with completion from the live connection, streaming results, and personal data masked in the grid.
- Shared query library and runbooks, so the queries above are saved once and the whole team follows the same steps.
- Scheduled queries, if you want the long-running list checked on a timer.
- Change timeline, to see what changed shortly before the operation started.
The query, the evidence and the change, in one window.
Quorum works across SQL Server, PostgreSQL, MySQL, MongoDB and more. Request a free 14-day trial with your email and we will email you to set it up.
Questions or a team? Contact us.