What it means
$group keeps every group, and everything it accumulates for each one, in memory until it has
seen the last input document. Each pipeline stage may use 100 MB (104,857,600 bytes). Past that,
$group can write temporary files to disk, but only if the aggregation allows disk use. This one
didn’t, so the server stopped it with code 292, QueryExceededMemoryLimitNoDiskUseAllowed.
MongoDB 8.0 says “didn’t allow external spilling; pass allowDiskUse:true to opt in”. Older servers said “didn’t allow external sort. Pass allowDiskUse:true to opt in.” Same error, same fixes.
Since MongoDB 6.0, disk use is allowed by default (allowDiskUseByDefault is true). So on a
self-hosted server you usually meet this error only when something turns it off, or on an older
server. On Atlas free and Flex clusters, Atlas ignores allowDiskUse and behaves as if it were
false.
Common causes
$push: "$$ROOT"or a large$push/$addToSet. Collecting whole documents into each group holds most of the collection in memory at once.- Grouping before filtering. The
$matchthat would cut the input comes after$group, or not at all. - Disk use turned off:
{ allowDiskUse: false }on the aggregation,allowDiskUseByDefaultset tofalse, or a server older than 6.0 whereallowDiskUse: truewasn’t passed. - An Atlas free or Flex cluster, which never writes temporary files.
- A high-cardinality group key (a user id, a timestamp) with large accumulators per group.
How to fix it
Filter and trim before $group
Put $match first so $group sees fewer documents (an index on the matched fields helps), and
$project away fields you don’t need:
db.orders.aggregate([
{ $match: { placedAt: { $gte: ISODate("2026-12-01") } } },
{ $group: { _id: "$customer", orders: { $push: { id: "$_id", total: "$total" } } } }
])
Accumulate less
Counting and summing take a few bytes per group; pushing documents takes their whole size. If you
only need totals, use $sum, $avg, $min, $max or $count. If you need a few documents per
group, $topN, $bottomN, $firstN and $lastN (MongoDB 5.2 and later) keep only n of them:
{ $group: {
_id: "$customer",
orders: { $sum: 1 },
spent: { $sum: "$total" },
latest: { $topN: { n: 3, sortBy: { placedAt: -1 }, output: "$_id" } }
} }
Allow disk use
On a self-hosted server or a dedicated Atlas cluster:
db.orders.aggregate(pipeline, { allowDiskUse: true })
Check the server’s default with db.adminCommand({ getParameter: 1, allowDiskUseByDefault: 1 }).
With Mongoose, Model.aggregate(pipeline).allowDiskUse(true). Spilling is slower than staying in
memory, so trim the pipeline first.
Watch the 16 MB document limit too
Even with disk use, each group’s result is one document and can’t exceed 16 MB. A $push that
gathers too much fails with BSONObjectTooLarge instead.
Reproduce it
A temporary MongoDB 8.0.32 container of our own (mongo:8.0, removed afterwards), with 150,000
orders of about 1 KB each (161 MB) for 50,000 customers; internalDocumentSourceGroupMaxMemoryBytes
at its default, 104857600.
db.orders.aggregate(
[{ $group: { _id: "$customer", orders: { $push: "$$ROOT" } } }],
{ allowDiskUse: false }
)
MongoServerError[QueryExceededMemoryLimitNoDiskUseAllowed]: PlanExecutor error during aggregation :: caused by :: Exceeded memory limit for $group, but didn't allow external spilling; pass allowDiskUse:true to opt in
Each of these returned with allowDiskUse: false still set:
$sumof the count and the totals instead of$push: 50,000 groups;$pushof only_idandtotal: 50,000 groups in 2.2 seconds;- a
$matchon one month (indexed) before the$groupwith$push: "$$ROOT": 8,921 groups in 0.2 seconds.
With disk use left at the default, the original pipeline returned 50,000 groups in 3.4 seconds, and
explain("executionStats") showed usedDisk: true and spills: 3.
In Inlet
Queries use mongosh syntax, so you can paste the pipeline and run it as
db.orders.aggregate([…], { allowDiskUse: true }), then trim it until it fits. A refused
aggregation shows the server’s message.