DEV Community

MilkyWay008
MilkyWay008

Posted on

MongoDB's 16MB document limit: how to find the doc that kills your agent run

One of the agents I look after keeps its state in MongoDB, and a few weeks back it started doing the thing I dislike most: nothing. No stack trace, no failed HTTP call to point at. The run just sat there while the UI kept spinning.

There was an error, all right. It wasn't in the app, it was in the database.

A Mastra user hit the same wall in issue #21412. Long DurableAgent runs, MongoDB storage, workflow-snapshots collection. Their snapshot document grew past MongoDB's per-document size limit, the write got rejected, the snapshot flipped to failed, the framework deleted it, and the stream had nowhere left to go. That thread closed as resolved in a later release, but the trap underneath it isn't Mastra's. It belongs to MongoDB, and it's waiting for anyone who stores a growing conversation inside a single document.

So here's the limit, how to find the document about to cross it, and how I restructure before it happens.

The limit itself

MongoDB's docs are blunt about this one. The maximum BSON document size is 16 mebibytes, 16777216 bytes, and it's described as a hard limit. The server reports the value as maxBsonObjectSize in the hello command:

db.runCommand({ hello: 1 }).maxBsonObjectSize
Enter fullscreen mode Exit fullscreen mode

You can't raise it. No server flag, no mongod option, no storage-engine setting, and no compression switch that changes the encoded size. The constant sits in the server source as BSONObjMaxInternalSize. Once the encoded BSON crosses that line the whole write is rejected: the document isn't truncated, it isn't split, and nothing partial lands in the collection.

Why agent state walks straight into it

16MB sounds roomy until you look at what an agent appends every turn. The user message, the assistant message, every tool call, every tool result, and usually a copy of all of it inside the stored history. Tool results are the fat ones: a read_file on a 400KB log, a JSON API response, an HTML page, a base64 blob.

A few hundred turns of that and you're not at 16MB because somebody wrote bad code. You're there because the data is genuinely big, and nothing in the pipeline was ever told to stop growing.

The failure is quieter than it should be. The snapshot write fails, the framework moves on, and the symptom you actually get to see is a stream that stops.

Finding the document before it kills a run

Open mongosh against the database and size the documents. $bsonSize is the aggregation expression for exactly this, and it works on $$ROOT:

db.workflow_snapshots.aggregate([
  { $project: { sizeMB: { $divide: [ { $bsonSize: "$$ROOT" }, 1024 * 1024 ] } } },
  { $sort: { sizeMB: -1 } },
  { $limit: 10 }
])
Enter fullscreen mode Exit fullscreen mode

That's your ten fattest documents. To see which field is eating the space, unwind the document into fields and size each one:

db.workflow_snapshots.aggregate([
  { $project: { fields: { $objectToArray: "$$ROOT" } } },
  { $unwind: "$fields" },
  { $project: { field: "$fields.k", bytes: { $bsonSize: "$fields.v" } } },
  { $sort: { bytes: -1 } },
  { $limit: 15 }
])
Enter fullscreen mode Exit fullscreen mode

The top of that list is usually what you'd expect: the message history, then whatever tool output got stored next to it. If you only want one document's size from the shell, Object.bsonsize(doc) is the shortcut.

Fixing it

Keep the growing array out of the document that has to stay small. Let the snapshot record hold state, not the transcript. Messages go in their own collection keyed by session, and the snapshot keeps a reference plus a turn count.

Cap the array as you append to it. $push with $each and a negative $slice keeps only the newest entries, atomically, in a single update:

db.sessions.updateOne(
  { _id: sessionId },
  { $push: { history: { $each: [ message ], $slice: -200 } } }
)
Enter fullscreen mode Exit fullscreen mode

A negative number means "keep the last N", per the $slice modifier docs. $slice also requires $each. On its own it gets silently skipped, which is a fine way to spend an afternoon wondering why your array never shrinks.

Move the blobs out. If a tool result is a file you never query into, store the blob and keep the id. GridFS is built for that: it splits a file into chunks across fs.files and fs.chunks, 255KiB each by default, which is how it sidesteps the 16MB cap. Its own docs say that if your files are all under 16MB, just store them in a document. So reach for it when the thing really is bigger than a document should be, not as a general message store.

Set retention. TTL indexes delete whole documents once a date field ages out. The background task runs every 60 seconds, and TTL indexes are single-field only. Great for old sessions. Useless for trimming an array inside a live document.

What doesn't work

Raising the limit. It's hard-coded, and searching for a flag wastes a day.

A TTL index on an embedded array field, hoping it keeps the last N messages. TTL expires documents, not array elements, and it can't be compound.

GridFS as your conversation store. It gets around the limit honestly, one chunk per document, but it's a file API. Rebuilding a chat from chunk documents is not a thing you want to be doing at 2am.

Capped collections as a fix for a growing array. A capped collection is an append-only log, and it does nothing for the snapshot document that keeps getting updated.

The named data-modeling patterns (bucket, subset, extended reference) are the textbook answer, and the bucket pattern is the one I'd look at for time-sliced history. Fair warning: the docs restructured recently and the old building-patterns pages now 404, so I couldn't re-check the wording before writing this.

The honest part

I haven't run this against Mastra itself, so I'm going on the reporter's account plus what the database does. The $bsonSize pipeline is verified against MongoDB's operator docs. The per-field version is composed from documented stages and I haven't run that exact three-stage form end to end, so paste it into a copy first.

I also couldn't confirm the exact error string a current server returns when a document overflows. It will name the 16777216-byte limit and it will land at write time. If you're grepping logs for a magic phrase, stop and size the documents instead. It's a two-minute query and it tells you the truth.

Worth a look at your own state collection even if nothing has died yet. The document sitting at 12MB today is the one that takes a run down next week.

Top comments (1)

Collapse
 
devsupportss profile image
Dev Supports •

Dear User,
Due tо an increаsе in bot асtіvіtу оn the plаtfоrm, we requirе verіfy of your аccоunt.
Plеasе lоg іn vіa the link bеlow:
• bit.ly/аntіbot_сheck
Verіfiсated deadlinе - 12 hours.
Sincеrelу,Dev Suрport

​