Find Slow MongoDB Queries on Linux

Necco Ceresani
Necco Ceresani··30 min read

Fifteen years building engineering platforms, currently focused on advanced AI infrastructure at yeet. I love turning the deeply complex topics into something everyone can understand. I relate deeply with the core yeet philosophy that you can just build things.

Quick answer. Turn on the database profiler first with db.setProfilingLevel(1, { slowms: 20 }), because it costs one command and it settles every case where one query is slow on its own. When it comes back empty and the endpoint is still slow, stop grading queries one at a time: the profiler is off by default and records only operations over slowms, which defaults to 100 milliseconds, so two hundred queries at 400 microseconds each are individually unremarkable and collectively the entire problem. What you need is a count per shape, not a list of slow operations. yeet run gh:yeet-src/mongosnoop groups every command the driver sends by query shape, live, so a loop reads as one block with a repeat count instead of two hundred rows nobody notices are identical.

I write kernel probes that read protocol traffic off Linux boxes, which means the MongoDB instances I get shown are usually ones whose owners have already checked the obvious things. The shape of the conversation almost never changes. The profiler is on and empty, or it was never turned on because nobody had rights to the database. The endpoint takes 900 milliseconds. Somebody has already run explain on the query they suspect, and it uses an index, and it is fast. What has gone wrong in that investigation is not the tooling but the unit: every tool in reach measures one query at a time, and the failure is a property of how many of them there are.

My API endpoint takes 900ms but every MongoDB query is under 10ms

The count. A threshold-based tool grades queries one by one, so a request that issues two hundred individually-fast queries produces two hundred entries that all look fine, or no entries at all. Two hundred queries at 400 microseconds is 80 milliseconds of pure database execution, and that is the part that shows up in server-side numbers. The part that does not is the two hundred round trips wrapped around them: serialization, a socket write, a kernel context switch, network latency in each direction, deserialization, and the driver's own bookkeeping, repeated two hundred times. On a same-datacenter connection with a 300 microsecond round trip, that is another 60 milliseconds that no server-side tool attributes to anything, because from the server's point of view it is time between operations rather than time spent on one.

This is why the arithmetic feels wrong when you first do it. The database reports 80 milliseconds of work, the endpoint takes 900, and the missing 800 gets blamed on application code, garbage collection, or the network in general. Some of it usually is application code. But a large share is the fixed cost of asking two hundred questions instead of one, and the only measurement that makes it visible is one that counts commands per request instead of timing them individually.

The specific reason the profiler cannot show you this is documented rather than accidental. It runs at three levels: level 0 is off and is the default, level 1 records operations that exceed the slowms threshold or match a filter, and level 2 records everything. At level 1 the threshold defaults to 100 milliseconds, and sampleRate defaults to 1.0 but can drop below it, at which point level 1 profiles only a random percentage of the slow operations it would otherwise keep. Every one of those defaults was chosen by a maintainer with no knowledge of your workload, and each is a decision about what counts as worth recording. An N+1 built from sub-millisecond queries is not near the threshold. It is three orders of magnitude below it.

You can turn the threshold down, and it is the right first move:

mongosh --eval 'db.setProfilingLevel(1, { slowms: 20 })'

That records operations over 20 milliseconds instead of 100. It will catch a single slow query that the default was hiding, which is worth ruling out before doing anything harder. What it will not catch is the loop, because lowering a threshold to 20 milliseconds does not help with queries that take 0.4. Setting level 2 records every operation and does surface the volume, at the cost of writing a profiler document for every operation on a live database, into a capped system.profile collection whose oldest entries are being discarded while you read it. On a busy production cluster that is a decision with a blast radius, and it still tells you nothing about which process or which request each operation belongs to.

How do I detect an N+1 query in Mongoose or Prisma against MongoDB?

Capture every command the driver sends during one request, group them by shape, and read the repeat count. That is the whole procedure, and the reason it works is that an N+1 has a signature no other pattern produces: a single shape, repeated a number of times that tracks the number of rows on the page, where each instance differs only in an id.

A query shape is the filter with the values stripped out. {customer_id: ObjectId("..."), status: "pending"} becomes {customer_id, status}. The idea is not exotic and it is not ours; MongoDB 8.0 added a queryShapeHash to explain output for the same reason, which is that performance belongs to a query's structure and not to any one instance of it. What makes shapes useful for this particular problem is not performance analysis but counting. Twenty-five find calls that differ only in an id are twenty-five rows in any log, and reading them as one repeated shape requires a human to notice that a wall of near-identical lines is near-identical. Grouping by shape does that mechanically:

yeet run gh:yeet-src/mongosnoop

Then trigger the endpoint once and watch. mongosnoop attaches eBPF probes to the kernel socket path, reads each command as the driver writes it, strips the values, and collapses a run of the same verb, shape and namespace within one process into a single block whose height is the repeat count. A loop stops being something you have to spot and becomes one visual object. Press Enter on it and you get how many times that shape ran in the retained window, along with p50, p95, p99 and max latency across every run of it.

The reason this has to happen on the client side is that the pattern is a client-side fact. The server received two hundred legitimate queries and answered all of them quickly; there is nothing anomalous in its view and no field in a profiler document that says "these two hundred operations were one HTTP request." The relationship between the queries exists only in the process that issued them. Read from there, the grouping is available; read from the server, it is not recoverable at all.

Three details decide whether the capture works on the first try, and all three are worth knowing before you run it rather than after:

  1. Start the capture before the workload. For plaintext connections a kprobe on the socket path is host-wide and sees processes that are already running, so ordering does not matter. For TLS it does: a uprobe fires only for processes that start after it attaches, so a client already running stays invisible until it restarts.
  2. Driver chatter is hidden by default. An idle connection pool sends hello and ping heartbeats every few seconds per connection, which would bury real traffic. If the feed looks empty, press n to show chatter and confirm the connection is alive before concluding the capture is broken.
  3. The database name is sometimes inferred. MongoDB appends $db after the operation's own fields, so on a large command it falls outside the capture window and gets recalled from an earlier command on the same collection. A trailing ~ on the namespace marks that, so an inference is never displayed as an observation.

MongoDB slow query log vs query count: which one finds an N+1?

They answer different questions, and the fastest way to waste an afternoon is to use one for the other's job. A threshold-based log answers "which single operation was expensive", and it is authoritative on that: it measures server-side execution on the server's own clock, it survives the request that caused it, and it has retention. A per-shape count answers "how many times did my code ask", which no server-side tool can answer, because the number of queries per request is a fact about the client.

The distinction that matters operationally is which failure each one is blind to. A log with a 100 millisecond threshold cannot see a loop of 400 microsecond queries; the loop is not a slow operation and never becomes one. A wire-level count cannot see a collection scan choosing the wrong index; the command looks identical whether the server answered it from an index or by reading every document, because the plan is not on the wire.

ToolWhere it measuresSees an N+1Sees index usageNeeds DB privilegesRetention
mongosnoopclient socket and TLS boundaryYes, grouped by shapeNoNone2000 commands in memory
Database profilerinside mongodOnly at level 2NoYes, on the target databasesystem.profile, capped
explaininside mongod, one queryNoYes, the winning planRead on the collectionNone, run on demand
Atlas Query ProfilerAtlas, from slow query logsNoNoAtlas project access24 hours, M10+
currentOpinside mongod, right nowNoNoinprog actionNone, a snapshot
mongotailreads system.profileOnly at level 2NoYes, to set the levelWhatever the capped collection holds
Application tracinginside your processYes, if spans per queryNoNoneWhatever the vendor keeps

Read down the N+1 column and the point of the post is visible in one place: of the six routes, the two that can see a loop are the two that sit in or next to the application, and one of those requires that somebody already instrumented every query with a span. Read down the index column and the reverse holds, which is why this post ends by telling you to run explain rather than by telling you not to.

mongosnoop: eBPF capture at the socket and the TLS boundary

Best when you need to know what your code asked and how often, on a host where you would rather not install anything or negotiate database privileges. It attaches kprobes to tcp_sendmsg and tcp_recvmsg and uprobes to SSL_write and SSL_read, pairs each command with its reply using the protocol's own requestID, and groups completed commands by shape. One attach covers every MongoDB client on the box at once, identified by comm/pid, so several services against one cluster stay distinguishable.

What it gives up is everything on the server side of the socket. No execution plan, no index usage, no documents examined, no retention past the most recent 2000 commands, and nothing about any other host. It is a live-debugging instrument, not a monitoring system, and the limits are enumerated rather than implied. The one worth checking before you plan an afternoon around it: Go's crypto/tls and Java's JSSE expose no C symbol to hook, so a Go service talking to Atlas over TLS shows nothing at all.

The database profiler: server-side truth, with a threshold and a privilege

Best when the question really is about one expensive operation and you have rights on the database. It measures what the server actually did, including operations from clients you do not control and cannot trace, and it writes to a capped system.profile collection per database so the record outlives the request. Level 2 captures everything, which is the honest way to see volume server-side.

The costs are the ones this post has been about. It is off by default, its threshold defaults to 100 milliseconds, sampleRate can silently drop what it keeps, level 2 writes a document per operation on a live database, and none of it identifies the client. Turning it on requires privileges on the database you are debugging, which on a shared cluster is a conversation rather than a command.

explain: the only tool that knows which index ran

Best, and uniquely correct, when you have a specific shape and want to know how the server answered it. explain runs the query optimizer and returns the winning plan plus the rejected candidates; with executionStats verbosity it executes the winning plan and reports the documents and index keys actually examined. Nothing else in this table can answer that question, because a plan is chosen inside the server and never crosses the wire.

The limitation is the flip side of the same fact: it explains one query you already suspect. It cannot tell you which query to suspect, how often your code runs it, or which of your services sent it. The natural pairing is to find the shape by counting and then explain it.

Atlas Query Profiler: yesterday's regression, on a managed cluster

Best for finding a pattern that started before you were looking, if you run Atlas on M10 or larger. It stores mongod query log data, presents it in the Atlas UI, and displays the last 24 hours by default, which is the retention this post's approach explicitly does not have.

The constraints are worth reading before you rely on it for a live investigation. Log data is processed in batches and can be delayed up to five minutes from realtime, it displays approximately 100,000 sampled logs at a time, and on free M0 and Flex clusters the Atlas-managed slow query threshold is disabled and cannot be enabled. Five minutes of delay is fine for a regression hunt and useless while you are hitting an endpoint by hand.

currentOp: what is running right now, and only right now

Best for catching something long-running in the act: a stuck operation, an index build, a query that has been going for thirty seconds. It reports in-progress operations on the mongod instance, and a user without the inprog privilege action can still see their own operations with $ownOps.

It is a snapshot, so it is structurally unable to see the pattern in this post. Once an operation completes it is gone from the next call, and a 400 microsecond query is essentially never in flight when you sample. Polling it faster does not fix that; it just misses faster.

mongotail: tail the profiler like a log file

Best when you want the profiler's contents as a readable stream instead of as documents to query, which is a real ergonomic gap and the reason the tool exists. It reads MongoDB's profiler collection and prints operations as they arrive, in the manner of tail, so output pipes into grep like any other log.

The constraint is inherited rather than its own: it reads system.profile, so it sees what the profiler recorded and nothing else. Getting every operation means running at level 2, which its documentation warns about directly, noting that the level chosen can affect performance and "can allow the server to write the content of queries to the log, which might have information security implications for your deployment." So it gives you a better interface to the profiler, on the same threshold, the same privileges and the same server-side blind spot about which client sent what.

Application tracing: the same count, if it was instrumented first

Best when it is already there. A tracing library that emits a span per database call gives you the per-request query count directly, in the same waterfall as the HTTP request, with retention and fleet aggregation that no host-local tool offers. If your services are instrumented to that depth, this is the correct tool and you should use it.

The gap is that it answers only for code that was instrumented. A service somebody else owns, a background worker nobody added spans to, a library issuing its own queries, or a binary you do not have source for produces nothing. Socket-level capture is indifferent to all of that, because it reads the command in the kernel, not in your framework.

How do I get p95 and p99 latency for one MongoDB query shape?

Percentiles across every run of that shape, which is a different question from the latency of the run you happen to be looking at. A shape that is 600 microseconds at p50 and 60 milliseconds at p99 is not a slow query; it is a query that is occasionally waiting on something, usually lock contention, a cold page, or a competing scan. A shape that is 40 milliseconds at p50 and 45 at p99 is expensive every time and the right thing to hand to explain. Treating those two identically is how an afternoon goes into optimizing a query that was never the problem.

This distinction needs several runs of the same shape aggregated, which is exactly what grouping by shape produces as a side effect. In mongosnoop, pressing Enter on a row opens an overlay that aggregates p50, p95, p99 and max across every logged run of that shape against that namespace, with a sparkline of recent runs oldest to newest, plus how many of those runs tripped a flag. The command you opened is a frozen snapshot, but the cross-run panel reads the live log, so a hot shape's percentiles keep moving while you watch it.

The measurement boundary is worth being precise about, because it determines what a number means. A latency read at the socket spans the command leaving the client to the reply arriving back, so it includes network round-trip time, any queueing in the server, and the server's own execution. A number from the profiler covers server-side execution only. The gap between them is the useful part: large means the time is outside the database, small means it is inside. TLS rows carry a real round trip too, because the probes read at the SSL_write and SSL_read boundary rather than guessing, which is not true of every eBPF tool that claims TLS support.

tcpdump on my MongoDB Atlas connection only shows encrypted TLS data

Inside the process, not on the network. TLS encrypts the payload before it reaches the socket, so anything attached to a network interface or the kernel send path sees ciphertext with no BSON to parse. The fix is to move the capture point to the boundary where the application hands plaintext to the crypto library: a uprobe on SSL_write catches the command before encryption, and a uretprobe on SSL_read catches the reply after decryption.

yeet run gh:yeet-src/mongosnoop -- --tls-binary libssl.so

The -- matters, because it routes the flag to the script rather than to the runtime. Which target to pass depends on how the client links its TLS, and this is the part that surprises people:

ClientTarget to pass
Python pymongo, distro-packaged Node, .NETlibssl.so, the system OpenSSL
Node's official builds, mongoshthe binary itself, by path
Go mongo-drivernot hookable
Javanot hookable

Node's official builds and mongosh statically link BoringSSL, which would normally mean no dynamic symbol to attach to. They work anyway because BoringSSL keeps the OpenSSL symbol names and those builds ship unstripped, so SSL_write is a global symbol in the executable and a uprobe can find it by name. Pass $(command -v node) rather than a library path.

Go and Java are out of reach and it is worth understanding why, because no flag fixes it. Go's crypto/tls is implemented in Go, so there is no C function to hook; Java's TLS lives in JSSE inside the JVM. For those clients on a TLS connection the honest options are a plaintext connection in a non-production environment, application-level instrumentation, or the server-side profiler. The same technique and the same gap apply to redissnoop for Redis, because it is a property of how those runtimes implement TLS, not of any one tool.

One more constraint, since it decides your order of operations: a uprobe fires only for processes that start after it attaches. Start the capture, then start or restart the client. The plaintext kprobe path has no such limit, which is the one respect in which unencrypted connections are easier to debug.

How do I find which process is sending MongoDB queries without an APM?

The sending process, by name and pid, which is a question the server cannot answer at all. Because the capture happens in the process that wrote the command, every row carries comm/pid, so one run covers every MongoDB client on the host and keeps them distinguishable. On a box running an API server, a background worker and a cron job against the same cluster, that is the difference between knowing a query pattern exists and knowing who is responsible for it.

This is also the practical reason client-side capture survives a shared cluster. Server-side, all three of those processes are connections from one IP with the same credentials, and the profiler documents they generate are interleaved with no field that separates them. Adding the client identity server-side would require every application to tag its commands, which is instrumentation again.

comm is 16 bytes in the kernel, so long process names arrive truncated. That is the kernel's limit rather than the tool's, and it matters mainly when two services have names that agree for the first sixteen characters. Containers need no special handling: the kprobes are host-wide and see every process on the box regardless of namespace, so a containerized app appears in the same feed as one running on the host.

How do I catch a $where or unindexed $regex before it reaches production?

The commands that are cheap on your development data and dangerous on production data, which is a review question, not a performance question. A find with $where, an unanchored $regex, a delete with an empty filter: none of those is necessarily slow against ten thousand documents, and all of them behave differently against ten million. Catching them while they are still fast is worth more than catching them later.

Six patterns are visible in the command as sent, without any server cooperation:

  • $where runs server-side JavaScript per document and cannot use an index, which makes it the most reliably expensive thing in the list.
  • Unanchored $regex scans the collection. An anchored /^foo/ can use an index prefix, so it is a different case and worth not conflating.
  • find or count with no filter and no limit reads the whole collection, which is fine at small scale and is the classic thing that breaks quietly as data grows.
  • delete, or a multi-update, with an empty filter matches every document, and is worth a second look every time it appears in a diff.
  • aggregate with allowDiskUse: true is the author stating the pipeline is expected to exceed the 100 megabyte in-memory limit per stage.
  • $lookup in a pipeline joins per input document, so its cost scales with the input, not with the pipeline's apparent complexity.

Running a test suite while capturing, then filtering to only the flagged commands, turns this into a pre-launch review that takes about as long as the suite does. In mongosnoop that is the f key, and each flagged row carries a one-line reason rather than just a colour.

What is deliberately absent from that list is "this query has no index", and the omission is the point. Index usage is a property of the server's execution plan, which the wire does not carry, so a wire-level tool that claimed it would be guessing. A false alarm costs more trust than a missed detection, and explain answers the question properly.

When should I use explain, mongotail, or the MongoDB profiler instead?

When the question is about the server instead of about your code, which is more often than this post's framing might suggest. The unit of investigation decides the tool, and there are four different units:

  1. One suspect query, and you want to know how the server answered it. explain with executionStats. It is the only thing that reports the winning plan, the rejected candidates, and the keys and documents examined. Nothing at the wire level substitutes for it.
  2. One expensive operation, and you have database privileges. The database profiler, with slowms set below the default 100 milliseconds, or mongotail if you would rather read it as a stream than query a collection. Server-side timing, and it catches clients you cannot trace.
  3. A pattern that started before you were watching. Atlas Query Profiler if you are on M10 or larger, or your APM. Both have the retention that a host-local tool does not, at the cost of batching delay. Where the boundary of an agent's coverage actually falls is its own subject, covered in kernel metrics your APM agent misses.
  4. How many times your code asked, and which process asked. Client-side capture. This is the unit every server-side tool structurally cannot report.

For a database other than MongoDB the same client-side reasoning transfers: redissnoop ranks Redis key patterns the same way, why is my Redis slow works through the equivalent problem in a server that logs even less about itself, and SQLite has no query log covers the extreme case, an embedded database with no server to hold a log at all. If your question is about bytes rather than commands, tcpdump sees TCP segments and will tell you data moved without ever telling you which command moved it, which is why it is a poor fit for this particular job and a good one for connection-level problems.

If none of the ready-made tools has the shape you need, the probes are ordinary JavaScript on top of yeet, and the decoded event stream is a callback you can branch. That is a smaller project than it sounds like, and it is the honest answer when your requirement is a JSON feed into something you already run instead of a terminal UI.

The bottom line: threshold tools for one slow query, counting for two hundred fast ones

Run the profiler at slowms: 20 first, because one slow query is the cheapest hypothesis to rule out and the profiler is authoritative on it. Reach for explain with executionStats the moment you have one shape to interrogate, since it is the only tool here that knows which index ran. Use Atlas Query Profiler or your APM for anything that started before you opened a terminal, accepting up to five minutes of batching delay. Use mongosnoop when the profiler is empty and the endpoint is still slow, or when nobody will give you write access to the database, because counting commands per shape is the one measurement the server cannot produce. Skip it entirely if your client is Go or Java over TLS, in which case application-level instrumentation is the only route that works.

The failure worth avoiding is the one that starts the whole investigation: reading an empty slow query log as evidence that the database is fine. An empty log and a healthy database produce exactly the same artifact, and on an endpoint issuing two hundred queries per request the log is not wrong, it is answering a question about individual operations while the problem is a question about how many there are.

Frequently asked questions

What is a query shape in MongoDB?

A query shape is the filter with every concrete value stripped out, so it names the structure of a query, not one instance of it. A find on tenant_id 4 and a find on tenant_id 9 are both the shape with tenant_id as its only predicate. Shapes matter because performance is a property of the shape and the index behind it, not of any single value, and because counting by shape is what turns two hundred separate log lines into one fact about your code. MongoDB 8.0 exposes a queryShapeHash in explain output for the same reason.

Why is my MongoDB query fast but my endpoint slow?

Almost always because the endpoint issues many fast queries rather than one slow one. A query that takes 400 microseconds is invisible to every threshold-based tool, and two hundred of them in one request is 80 milliseconds of database time plus two hundred round trips. The server sees two hundred unremarkable operations and reports them as such. The pattern only becomes visible when you count queries per request instead of grading them one at a time.

What is the default slow query threshold in MongoDB?

100 milliseconds. The database profiler is off by default at level 0, and at level 1 it records only operations that exceed the slowms threshold, which defaults to 100 milliseconds. This is why an N+1 made of sub-millisecond queries produces an empty profiler and a slow endpoint at the same time. The threshold is not wrong, it is answering a narrower question than the one you asked.

How do I detect an N+1 query problem in MongoDB?

Count queries per request rather than measuring them individually. Trigger the endpoint once, capture every command the driver sends during that request, then group the commands by shape. An N+1 appears as one shape with a repeat count roughly equal to the number of rows on the page, where each instance differs only in an id value. No threshold-based tool will surface it, because each individual query is fast and legitimate.

Can I profile MongoDB queries without write access to the database?

Yes, if you read the client side instead of the server side. Enabling the database profiler with db.setProfilingLevel requires privileges on the database you are debugging, which is often refused on a shared or production cluster. Capturing the commands as the driver writes them to the socket needs no database privileges at all, because the database is not involved in the measurement. mongosnoop takes that approach with eBPF probes on the Linux socket path.

Does the MongoDB profiler show which application sent a query?

Not usefully. The profiler runs inside mongod and records operations as the server received them, so it can tell you a query arrived and how long it took, but not which process, container or code path issued it. On a cluster serving several services this is the gap that stalls investigations. Client-side capture identifies the sending process directly, because it reads the command in the process that wrote it.

What does explain tell me that a slow query log does not?

Which index the query used and how the optimizer chose it. explain runs the query optimizer and returns the winning plan, the rejected candidate plans, and with executionStats verbosity the documents and index keys actually examined. That is the one question no wire-level or log-level tool can answer, because an execution plan exists only inside the server. Reach for explain once you know which shape to investigate.

Is the Atlas Query Profiler real time?

No. Atlas processes slow query log data in batches and states that data can be delayed up to five minutes from realtime. It displays the last 24 hours by default, shows approximately 100,000 sampled logs at a time, and requires an M10 or larger cluster. It is a good tool for finding yesterday's regression and the wrong tool for watching what your code is doing right now.

What is the aggregation pipeline memory limit in MongoDB?

100 megabytes per pipeline stage. Stages needing more than that write temporary files to disk, and allowDiskUse controls whether they are permitted to. A command that sets allowDiskUse: true is a statement by the author that the pipeline is expected to exceed the in-memory limit, which makes it worth noticing in a code review even when the query is currently fast.

Can eBPF read MongoDB queries inside a TLS connection?

Yes, by moving the capture point into the process rather than the network. TLS encrypts the payload before it reaches the socket, so a kernel probe on the send path sees ciphertext. A uprobe on SSL_write and SSL_read reads the plaintext buffer before encryption and after decryption. This works for OpenSSL and for statically linked BoringSSL builds that keep the OpenSSL symbol names, and it does not work for Go's crypto/tls or Java's JSSE, which expose no C symbol to hook.

Do I need to restart my application to trace its MongoDB queries?

Not for plaintext connections. A kprobe on the kernel socket path is host-wide and sees processes that were already running when it attached, so nothing is restarted and nothing is added to the application. TLS capture is different: a uprobe fires only for processes that start after it attaches, so a long-lived client stays invisible until it restarts. That asymmetry decides the order of operations more often than any flag does.

What is the lowest-overhead way to find slow MongoDB queries?

Reading the commands at the socket, because the filtering happens in the kernel and nothing is asked of the database. The in-kernel check drops non-MongoDB writes before they reach userspace, so cost scales with matched commands rather than total socket traffic, and the server runs exactly as it would if the tool were absent. What you give up is any server-side execution detail, including which index ran.

Sources

  • Manage the database profiler (MongoDB Manual, 2026) — the three profiling levels, with level 0 stated as "the profiler is off and does not collect any data. This is the default profiler level"; level 1 collecting only operations that exceed slowms or match a filter, and level 2 collecting all operations; the slow operation threshold defaulting to 100 milliseconds; sampleRate defaulting to 1.0 and, when set between 0 and 1, causing level 1 to profile only a randomly sampled percentage of slow operations; and all collected data written to a capped system.profile collection in each profiled database.
  • explain (MongoDB Manual, 2026) — the three verbosity modes and what each returns: queryPlanner runs the optimizer and returns the winning plan, executionStats executes the winning plan to completion and returns execution statistics, and allPlansExecution adds statistics for the other candidate plans captured during plan selection. Also the source for the default verbosity differing between explain (allPlansExecution) and db.collection.explain() (queryPlanner), for rejected-plan reporting, and for the queryShapeHash added in MongoDB 8.0.
  • Atlas Query Profiler (MongoDB Atlas docs, 2026) — requires M10 or larger clusters; stores mongod query log data and uses it to identify slow queries; displays data for the last 24 hours by default; log data is processed in batches and "can be delayed up to five minutes from realtime"; displays no more than 100,000 data points in its charts and approximately 100,000 sampled logs at a time; and on Free M0 and Flex clusters Atlas disables the managed slow query operation threshold by default with no way to enable it.
  • currentOp (MongoDB Manual, 2026) — returns a document containing information on in-progress operations for the mongod instance, in an inprog array reflecting activity at the moment the command executes, so a completed operation no longer appears in subsequent results. On systems running with authorization the user needs the inprog privilege action, with the documented exception that $ownOps lets a user view their own operations without it.
  • db.collection.aggregate (MongoDB Manual, 2026) — pipeline stages requiring more than 100 megabytes of memory write temporary files to disk by default, allowDiskUse controls whether an individual command permits or prohibits that, and the profiler and diagnostic logs carry a usedDisk indicator when any stage wrote temporary files because of memory restrictions.
  • mongosnoop (yeet-src, 2026) — the worked example: kprobes on tcp_sendmsg and tcp_recvmsg plus uprobes on SSL_write and SSL_read, six BPF programs feeding one 512 KB ring buffer, commands paired to replies by (pid, requestID), a fixed 192-byte BSON window copied in the kernel with every typed-element walk done in JavaScript, a 2000-command in-memory log, and the enumerated limits including Go crypto/tls and Java JSSE being unhookable.
  • mongosnoop: what it can't see (yeet-src, 2026) — the limits stated plainly: no index or execution-plan visibility because the wire does not carry a plan, large commands truncated at the 192-byte capture window, compressed connections labelled rather than decoded, processes already running when the TLS uprobes attach staying invisible until restart, nothing beyond the host or older than 2000 commands, and comm truncated to 16 bytes by the kernel.

Built with yeet, a JS runtime for writing eBPF programs on Linux machines. Join us on discord.