New to Kestra?
Use blueprints to kickstart your first workflows.
Detect MongoDB arrays growing toward the 16MB BSON limit, archive the overflow, trim to the newest entries behind an approval, then re-measure.
A MongoDB document cannot exceed 16MB. An embedded array that every write $pushes to, the
events list on a device, the history on an order, the audit trail on an account, has
nothing stopping it. It grows for months, then one document crosses the limit and from that
moment every write to it fails with BSONObjectTooLarge. The failure is per document, so it
hits the busiest records first while the rest of the collection looks fine, which is why it
is usually found in production rather than in review.
This blueprint measures the thing that actually matters: how many entries each array holds
and how close the whole document is to the ceiling. It reports the documents over a length
threshold, can copy the entries it is about to discard into an archive collection, trims each
oversized array to its newest entries with one server-side $push carrying $each: [] and a
negative $slice, then measures again and fails the run if anything is still oversized.
This blueprint was created by zkasuran.
auto_remediate true, dry_run false, then a human answering the Pause.length_threshold is never written to, so a short array cannot be clipped by a run that was
aimed at a long one.keep_n is refused before the first read when it is below 1, which would empty the array,
or above length_threshold, which could never clear the detection.archive_overflow true the discarded entries are written out and counted first.
The run then fails before the trim if the archive holds fewer entries than the detection
says will be dropped. No entry is dropped without its copy already stored.archive_overflow is false. The flow
says so in its own log before it writes.check_inputs (io.kestra.plugin.core.flow.If) fails the run on a keep_n that is below
1 or above length_threshold, before any connection is made.detect (io.kestra.plugin.mongodb.Aggregate) matches documents where the array field is
an array and the array index at length_threshold exists, which is how MongoDB answers
"longer than N" without walking every entry. One $facet then returns the exact document
count, the exact number of entries a trim would discard, the largest document in bytes with
its percentage of the 16777216 byte limit and the worst scan_limit documents by length.report (io.kestra.plugin.core.log.Log) logs those numbers, because task outputs are not
in the execution JSON.route (io.kestra.plugin.core.flow.If): zero oversized documents logs clean and stops.
One or more sends alert to Slack and prints planned, which states what a remediation run
would do on every run including a dry one.remediate_gate (io.kestra.plugin.core.flow.If) opens only with auto_remediate true and
dry_run false. hold (io.kestra.plugin.core.flow.Pause) waits for an answer.archive_overflow true, archive (io.kestra.plugin.mongodb.Aggregate)
slices the entries about to be discarded, $unwinds them and $merges one document per
entry into archive_collection, keyed by source document, execution id and array index.
archive_check counts what landed and archive_gate fails the run if it is short.trim (io.kestra.plugin.mongodb.Update, UPDATE_MANY) applies
$push: { <field>: { $each: [], $slice: -keep_n } }, the server-side way to keep the newest
keep_n entries, to every document matching the detection predicate.reverify runs the same measurement and verify_gate fails the run with
io.kestra.plugin.core.execution.Fail if any document is still over the threshold. Without
approval declined records the decision and nothing changes.errors posts to Slack when any task fails, for example when MongoDB is unreachable.BSONObjectTooLarge
incident on the busiest records.BSONObjectTooLarge at 3am.MongoDB has no server-side cap on array length. $slice on a push bounds it, but only if every
writer remembers to use it. It also does nothing about the documents that already grew. This
flow is the outside check. The schedule decides when growth is measured, the counts are logged,
the write sits behind auto_remediate, dry_run and a Pause with the decision recorded on
the execution, the archive step is proved before anything is discarded and the re-measurement
turns the trim into a checked outcome. The Slack alert and the error path are the same ones the
rest of your flows use.
On Kestra 2.0.5 OSS against a MongoDB 7.0 container, on the final file. The collection held
three documents: device-hot with 10050 entries in events, device-quiet with 5 entries
plus device-noarray with no events field at all. Every before and after length was read back
with mongosh, not only from the Kestra log.
| Run | Inputs | Result |
|---|---|---|
| Bad config | keep_n: 6000, length_threshold: 5000 |
check_inputs failed the run before any read, errors alerted |
| Clean | length_threshold: 20000 |
0 oversized, clean logged, no alert, SUCCESS. mongosh 10050 / 5 |
| Detect and dry run | length_threshold: 5000, other defaults |
1 oversized, 9050 entries over, largest 1426096 bytes which is 8.5 percent of the limit, Slack alert, nothing written. mongosh still 10050 / 5 |
| Armed but dry | auto_remediate: true, dry_run: true |
Still no write. mongosh still 10050 / 5 |
| Declined | auto_remediate: true, dry_run: false, resumed approved: false |
Paused, then declined. mongosh still 10050 / 5 |
| Remediate | the same, resumed approved: true |
Matched 1, modified 1, re-check 0 oversized, SUCCESS. mongosh 1000 / 5, the document 1426096 bytes to 140946 |
| Archive then trim | the same plus archive_overflow: true |
9050 entries archived and counted before the trim, then matched 1, re-check 0. mongosh archive 9050, collection 1000 / 5 |
| Still unbounded | remediate with a writer inserting oversized documents through the run | Trim matched 48, re-check still found 1 oversized, verify_gate failed the run, errors alerted |
| Unreachable | connection_uri pointed at a dead host |
detect failed, errors posted to Slack, execution FAILED |
The trim kept the newest entries, not an arbitrary 1000: mongosh read back sequence numbers
9050 to 10049 out of the original 0 to 10049. The archive held the other side exactly, sequence
0 to 9049, one document per entry. device-quiet stayed at 5 entries and device-noarray was
never matched in any run, which is the precision that matters: a trim aimed at one document
must not clip the others.
The clean run and the detect run isolate it. Nothing changed on the collection between them
except the threshold: 20000 returned 0 oversized, 5000 returned exactly 1, the same
device-hot, on the same three documents. mongosh confirmed both counts directly. The
still-unbounded run proved the gate the other way: with a writer inserting oversized documents
through the run, the trim landed on 48 documents yet the re-check still found 1, so the run
failed instead of reporting a clean collection.
$bsonSize.docker run -d --name mdb --network mynet mongo:7.0, seed one document with a
long array and one with a short one, then run with connection_uri: mongodb://mdb:27017.MONGODB_URI: the MongoDB connection string. Read when connection_uri is left empty,
which is how the flow should run in production. In Kestra OSS, provide it as the
base64-encoded environment variable SECRET_MONGODB_URI.SLACK_WEBHOOK_URL: used by alert and the errors block. Provide it as
SECRET_SLACK_WEBHOOK_URL.| Input | Default | Purpose |
|---|---|---|
connection_uri |
empty | Plain connection string for a local test. Empty reads MONGODB_URI |
database, collection |
appdata, activity |
The collection to guard |
array_field |
events |
The embedded array that grows by append |
length_threshold |
5000 |
More entries than this counts as oversized. Also the trim filter |
keep_n |
1000 |
Entries kept when trimming, the newest ones |
scan_limit |
25 |
How many documents the report names. Counts stay exact |
archive_overflow |
false |
Copy discarded entries out first, then verify before trimming |
archive_collection |
activity_overflow_archive |
Where archived entries land |
approval_timeout |
PT2H |
How long the pause waits for an answer |
auto_remediate |
false |
Allow the trim |
dry_run |
true |
Must be false for a trim to apply |
MONGODB_URI and SLACK_WEBHOOK_URL secrets.database, collection and array_field, then run the flow. It reports the oversized
count and the headroom. It writes nothing.keep_n from that report, set archive_overflow to true if the old entries matter,
set auto_remediate: true and dry_run: false, run again, then answer the pause.daily schedule, which always runs in safe mode.ceiling_pct crosses a
number, so one threshold covers arrays of very different entry sizes.io.kestra.plugin.core.flow.Loop over a
list, with concurrencyLimit: 1 on the part that writes.archived_at in the archive collection so the archive itself stays bounded.$slice themselves, so this flow
becomes the check rather than the cleanup.Pause with a Slack approval. Or drop it for a collection you are sure about.$push with $slice: https://www.mongodb.com/docs/manual/reference/operator/update/slice/$bsonSize: https://www.mongodb.com/docs/manual/reference/operator/aggregation/bsonSize/