Cardinality decides more than taste does, and three buckets cover almost every relationship you will meet. One-to-few — a few children with a known ceiling, such as addresses on a customer: embed them and stop thinking about it. One-to-many — hundreds to a few thousand, still bounded: embed only if you always read them with the parent and never sort them across parents, otherwise reference or keep a subset (Reference Patterns). Unbounded — audit entries, page views, IoT readings: always their own collection, for a reason worth measuring once.
db.feeds.drop(); db.feeds.insertOne({ _id: 'u1', events: [] });
const ev = () => ({ at: new Date(), kind: 'view', postId: 1234 });
const size = id => bsonsize(db.feeds.findOne({ _id: id }));
for (let round = 1; round <= 5; round++) {
try { db.feeds.updateOne({ _id: 'u1' },
{ $push: { events: { $each: Array.from({ length: 100000 }, ev) } } }); }
catch (e) { print('stuck at ' + size('u1') + ' bytes -- code ' + e.code + ': '
+ e.message.split(' :: caused by :: ')[1]); break; }
print(round * 100000 + ' events -> ' + (size('u1') / 1048576).toFixed(2) + ' MiB');
}
db.feeds.drop(); db.feeds.insertMany([{ _id: 'short', events: Array.from({length: 10}, ev) },
{ _id: 'long', events: Array.from({ length: 100000 }, ev) }]);
for (const id of ['short', 'long']) { const t = Date.now();
for (let i = 0; i < 200; i++) db.feeds.updateOne({ _id: id }, { $push: { events: ev() } });
print(id.padEnd(6) + ' 200 single $push: ' + (Date.now() - t) + ' ms'); }100000 events -> 4.85 MiB 200000 events -> 9.81 MiB 300000 events -> 14.77 MiB stuck at 15488920 bytes -- code 10334: Resulting document after update is larger than 16777216 short 200 single $push: 198 ms long 200 single $push: 10443 ms
A three-field event costs about 51.6 bytes of BSON, so the 16,777,216-byte document limit lands near 324,000 events. Because the result of the update must fit, the document freezes at 300,000 with error code 10334 and 1.2 MB of headroom it can never use — and from there it accepts no writes at all: a dead record you repair by hand.
The last two lines bite long before 16 MB. WiredTiger has no in-place array append: every $push reads the whole document, applies the change and writes it back. One append to a 100,000-element array cost 52 ms against 0.99 ms for the same append to a 10-element array — 53 times slower — and a multikey index over that array makes it worse, one key per element on every push.