Skip to content

How to find and kill long-running operations in MongoDB

Find long-running operations and idle transactions in MongoDB with $currentOp, stop them safely with killOp and killSessions, handle sharded clusters, and set maxTimeMS and transaction limits.

By Piyush Baheti · Updated

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

Client operations running for more than 30 seconds · MongoDB 5.0+ · run on admin
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 $currentOp can only run on the admin database, and $currentOp must be the first stage.
  • allUsers: true shows every user's operations, not only yours. With access control on, it needs the inprog privilege.
  • client: { $exists: true } keeps operations that came from a client connection and leaves out most background work. client is reported on standalones and replica sets; on mongos see step 6.
  • secs_running is only present while active is true. Use microsecs_running for 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.

Open transactions, oldest first · MongoDB 5.0+ · run on admin
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 } }
])
  • idleSessions defaults to true; it is set here so the intent is clear.
  • timeOpenMicros is the transaction's age. It equals timeActiveMicros plus timeInactiveMicros, so a large idleSecs means the client is holding the transaction open without sending work.
  • expiryTime is when MongoDB will abort the transaction by itself: its start time plus transactionLifetimeLimitSeconds, which defaults to 60 seconds. A transaction much older than a minute means someone raised that limit.
  • lsid is 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.

Everything about one operation · MongoDB 5.0+ · replace the opid
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: COLLSCAN means it is reading every document; IXSCAN { … } names the index.
  • appName, client and effectiveUsers: which program, host and database user started it. If these are empty or point at an internal user, leave the operation alone.
  • waitingForLock: true means it is waiting for another operation, so the long-running one may not be this one. lockStats.*.timeAcquiringMicros shows how long it has waited in total.
  • waitingForFlowControl: true on 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.
$currentOp output can contain personal data
The 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

Stop one operation · MongoDB 5.0+ · replace the opid
db.killOp(48213)
killOp stops work that may be half done
MongoDB's own documentation says to terminate running operations with extreme caution: only kill operations started by clients, never internal ones such as replication. A write that is not in a transaction and is stopped part way may have changed some documents and not others. Confirm the owner from appName, client and effectiveUsers first, and tell the team that owns the program.
  • It is not instant. killOp flags the operation, and killPending turns true. The operation ends at its next safe termination point. Run the query from step 1 again to confirm it is gone.
  • Privileges. On a mongod you can always kill your own operations. Killing another user's operation needs the killop privilege. 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.

End the session behind the oldest open transaction · MongoDB 5.0+ · run on admin
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 ] })
Never pass an empty array
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.

6. Sharded clusters

Run $currentOp on a mongos and it reports operations from every shard. The opid then has the form "<shard>:<opid>", and there is a shard field.

Long-running operations across shards · MongoDB 5.0+ · run on mongos
db.getSiblingDB("admin").aggregate([
  { $currentOp: { allUsers: true } },
  { $match: { active: true, secs_running: { $gte: 30 } } },
  { $project: { _id: 0, shard: 1, opid: 1, secs_running: 1, op: 1, ns: 1, appName: 1 } },
  { $sort: { secs_running: -1 } }
])

// a write with no session: kill it on each shard, from mongos
db.killOp("shardA:100913")
  • Reads. Find the opid with localOps: true on the same mongos the client used, then run db.killOp() there. MongoDB passes the kill to the shards.
  • Writes in a session. Use killSessions on the mongos with the lsid, as in step 5.
  • Writes without a session. Kill the "<shard>:<opid>" entry for each shard the write is running on.
  • One entry per node. From MongoDB 7.1, targetAllNodes: true returns one document per data-bearing node in each shard instead of one per shard.

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.

Per-operation limit with maxTimeMS · MongoDB 5.0+
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.

Default limit for reads · MongoDB 8.0+ · run on admin
db.adminCommand({
  setClusterParameter: { defaultMaxTimeMS: { readOperations: 30000 } }
})

db.adminCommand({ getClusterParameter: "defaultMaxTimeMS" })
  • Reads only. It applies to read operations such as find, count, distinct and aggregate (except pipelines with $merge or $out). The default is 0, meaning no limit.
  • A query's own maxTimeMS wins. Reports and analytics jobs that legitimately run longer must set their own maxTimeMS, or the default will stop them.
Transaction lifetime · MongoDB 5.0+ · each mongod
db.adminCommand({ getParameter: 1, transactionLifetimeLimitSeconds: 1 })

// back to the default of 60 seconds
db.adminCommand({ setParameter: 1, transactionLifetimeLimitSeconds: 60 })
Server settings change behaviour for every client
A lower 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.

IN QUORUM · SYNTHETIC DATA
Query Studio — shop-mongoprod · admin
opidappName · nsrunningplan · lock
48213nightly-export · shop.orders14 min 06 sCOLLSCAN
48877orders-api · shop.orders41 sIXSCAN · waiting for lock
idle txnbilling-sync · shop.invoicesopen 52 sidle 49 s
3 rows · streamedPII masked in grid
Long-running operations on a MongoDB connection in Query Studio. Illustrative, with synthetic data.
  • 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.
Do this in Quorum

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.

Do this in Quorum

Blocking, drawn for you.

Who waits for whom, with the root blocker, transaction age and owning program. No queries to remember at 2 AM.

Request a free trialFree 14-day trial on request, then from $12/month.
Rather practice first?A made-up outage · 5 min