Skip to content
Monghoul

BSONObjectTooLarge, and the field that is actually costing you the space

The 16 MB limit is on the encoded document, not on what you think you stored. The usual culprit is an array nobody bounded.

BSONObj size: 17825792 (0x1100000) is invalid. Size must be between 0 and 16793600(16MB)
· ·updated 13 September 2026 ·checked against MongoDB all supported versions
A wide rectangle divided into a coarse grid of large cells, with a solid red square about two cells wide hanging in the open space just above its top edge.
The document is already at the limit. The next field has nowhere to go, and the write is rejected rather than truncated.

Every MongoDB document has a hard ceiling of 16 MB, and it has never moved. When you cross it the write is rejected outright rather than truncated, which is the right behaviour and an abrupt way to find out.

The message names a size and a limit and nothing else. It does not say which field grew, which is the only thing you actually need.

The error code is BSONObjectTooLarge, code 10334. Worth knowing because a nearby but different error, “object to insert too large”, is a BadValue and comes from a different check, so searching the wrong one sends you to the wrong answers.

It is measured on the BSON, not on your JSON

The limit applies to the encoded document, and BSON is not the same size as the JSON you would print. Two things surprise people:

Every field name is stored in full, in every document. A field called customerShippingAddress costs 24 bytes of name per document, on top of its value, every time. In a collection of ten million documents that is 240 MB of uncompressed BSON, and inside a single document with a large array of subdocuments it is paid once per element. On disk it is less, because WiredTiger compresses blocks and repeated field names compress well, but the 16 MB document limit is measured on the uncompressed form, which is the number that matters here.

Every value carries a type byte and, for strings and subdocuments, a length prefix. A short string is not its character count. It is one type byte, the field name, a null terminator, a 4-byte length, the bytes, and another null.

So a document that looks small in a pretty-printed view can be meaningfully larger encoded, and a document with many small fields is worse per byte of actual data than a document with few large ones.

The cause is almost always an unbounded array

In practice, documents do not drift over 16 MB. They grow into it in one place: an array that has no limit on it, being appended to forever.

The pattern looks reasonable when written:

// events pushed onto the order as they happen
db.orders.updateOne(
  { _id: orderId },
  { $push: { events: { at: new Date(), type: 'scanned', by: userId } } }
);

That is fine for an order with twelve events. The order with 40,000 events is the one that fails, and it fails at write time, months later, on the busiest document you have.

The tell is that the failure is not spread across the collection. It is one document, or a handful, and they are always the oldest or the busiest.

Finding what is big

If you have the document, the direct route is to measure it:

db.orders.aggregate([
  { $project: { size: { $bsonSize: '$$ROOT' } } },
  { $sort: { size: -1 } },
  { $limit: 10 }
]);

$bsonSize needs MongoDB 4.4 or later. It gives you the encoded size of each document, so the ten worst offenders come back in one query.

To find out which field inside them is responsible, project the candidates separately:

db.orders.aggregate([
  { $match: { _id: theBigOne } },
  { $project: {
      whole: { $bsonSize: '$$ROOT' },
      // $bsonSize takes an object or null and ERRORS on anything else, an array included.
      // Wrapping the array in a document is how you measure it, and costs a few bytes of
      // wrapper against a field that is megabytes.
      events: { $bsonSize: { wrapped: '$events' } },
      eventCount: { $size: '$events' }
  } }
]);

That usually ends the investigation in one step, because one field is 95% of the total.

For a document you already have in hand, in a log or a failing test fixture, paste it into the BSON size calculator. It gives the exact encoded size, the share of the 16 MB limit, and a field-by-field breakdown, which is the same answer without needing a connection.

Fixing it

Three options, roughly in order of how often they are right.

Move the array into its own collection. An unbounded list of things that happened is a collection, not a field. One document per event, with an orderId, and the parent stops growing. It is the standard answer and it is standard because it works.

Bound the array deliberately. When you only ever read the last N, say so:

db.orders.updateOne(
  { _id: orderId },
  { $push: { events: { $each: [newEvent], $slice: -50 } } }
);

$slice with a negative number keeps the last 50 and discards the rest on every write. The document now has a ceiling by construction rather than by hope.

The bucket pattern, when you need all the history but not in one document: a new document per period, each holding a bounded window of events. More moving parts, and worth it for time series where you genuinely want the whole history queryable.

The check worth adding

None of this is hard once you know which document is large. The expensive part is finding out at write time in production.

If you have a collection with a growing array in it, the $bsonSize sort above is a two-minute query. Run it now rather than in six months, and the answer is either “everything is under 100 KB” or “one document is at 14 MB and nobody knew”.