Fix MongoServerError: document is larger than the maximum size 16777216

intermediate🍃 MongoDB2026-03-17| MongoDB 4.x–7.x, Node.js (Mongoose/native driver), Python (PyMongo), mọi hệ điều hành

Error Message

MongoServerError: document is larger than the maximum size 16777216
#mongodb#bson#document-size#16mb-limit

Chuyện gì đã xảy ra

Bạn cố gắng insert hoặc update một document và MongoDB trả về lỗi sau:

MongoServerError: document is larger than the maximum size 16777216

Con số đó — 16.777.216 bytes — chính xác là 16 MB. Đây là giới hạn kích thước document BSON cứng của MongoDB. Replica set, Atlas, máy dev cục bộ — không quan trọng. Giới hạn này được quy định thẳng trong đặc tả BSON và không có tùy chọn cấu hình nào để tăng lên.

Các nguyên nhân thường gặp:

  • Lưu trữ ảnh hoặc PDF đã mã hóa Base64 trực tiếp vào trường document — Base64 làm phình dữ liệu nhị phân thêm ~33%, nên một file PDF 12 MB đã chạm giới hạn trước khi bạn thêm bất kỳ trường metadata nào
  • Một mảng không giới hạn (log, sự kiện, lịch sử) cứ lớn dần theo mỗi lệnh $push cho đến khi vượt ngưỡng
  • Serialize một object graph lớn từ ORM rồi insert toàn bộ một lúc
  • Dữ liệu tích lũy nhiều tháng mà không ai theo dõi cho đến khi môi trường production gặp sự cố

Đo kích thước trước khi sửa

Đừng đoán mò trường nào bị phình to — hãy đo thực tế. Trong mongosh:

// Trong mongosh
const doc = db.mycollection.findOne({ _id: ObjectId("...") });
Object.bsonsize(doc);
// ví dụ: 18432000  ← vượt quá 16 MB

Trong Node.js với native driver:

const { BSON } = require('bson');
const size = BSON.calculateObjectSize(doc);
console.log(`Kích thước document: ${(size / 1024 / 1024).toFixed(2)} MB`);

Trong Python (PyMongo):

import bson
size = len(bson.encode(doc))
print(f"Kích thước document: {size / 1024 / 1024:.2f} MB")

Sau khi xác định được trường bị phình — thường là blob nhị phân hoặc mảng tăng trưởng vô hạn — hãy chọn giải pháp phù hợp với tình huống của bạn bên dưới.

Giải pháp 1 — Dùng GridFS cho dữ liệu nhị phân/file

Lưu file, ảnh hoặc PDF trực tiếp vào trường document là cách tiếp cận sai. GridFS được xây dựng chính xác cho mục đích này. Nó chia file thành các chunk 255 KB và lưu metadata riêng biệt, bỏ qua hoàn toàn giới hạn 16 MB.

Ví dụ Node.js (native driver):

const { MongoClient, GridFSBucket } = require('mongodb');
const fs = require('fs');

const client = await MongoClient.connect('mongodb://localhost:27017');
const db = client.db('mydb');
const bucket = new GridFSBucket(db);

const uploadStream = bucket.openUploadStream('report.pdf');
fs.createReadStream('/tmp/large-report.pdf').pipe(uploadStream);

uploadStream.on('finish', () => {
  console.log('ID file đã upload:', uploadStream.id);
});

Ví dụ Python (PyMongo):

from pymongo import MongoClient
import gridfs

client = MongoClient('mongodb://localhost:27017')
db = client['mydb']
fs = gridfs.GridFS(db)

with open('/tmp/large-report.pdf', 'rb') as f:
    file_id = fs.put(f, filename='report.pdf')
    print(f'ID file đã lưu: {file_id}')

Chỉ lưu file_id được trả về vào document chính. Truy xuất sau bằng bucket.openDownloadStream(file_id) hoặc fs.get(file_id).

Giải pháp 2 — Tách document (bucket pattern)

Mảng tăng trưởng vô hạn là thủ phạm điển hình. Bucket pattern giới hạn mỗi document ở N phần tử, rồi tạo document mới — đây là cách tiếp cận phổ biến cho event log, telemetry và dữ liệu time-series.

// Thay vì một document với 100k log entry:
// { _id, userId, events: [ ...100000 phần tử... ] }

// Dùng các document theo bucket:
// { _id, userId, bucket: 1, count: 200, events: [ ...200 phần tử... ] }
// { _id, userId, bucket: 2, count: 200, events: [ ...200 phần tử... ] }

const MAX_BUCKET_SIZE = 200;

await db.collection('user_events').updateOne(
  { userId: userId, count: { $lt: MAX_BUCKET_SIZE } },
  {
    $push: { events: newEvent },
    $inc: { count: 1 },
    $setOnInsert: { bucket: Date.now() }
  },
  { upsert: true }
);

Mỗi document luôn nằm dưới 16 MB. Truy vấn theo khoảng thời gian cũng nhanh hơn vì bạn quét các document nhỏ có giới hạn thay vì một document khổng lồ duy nhất.

Giải pháp 3 — Dùng reference thay vì embed

Nhúng sub-document hoạt động tốt với dữ liệu nhỏ, ổn định. Với dữ liệu tăng trưởng theo thời gian — đánh giá, bình luận, audit log — việc nhúng trở thành gánh nặng. Hãy chuyển dữ liệu đang tăng sang collection riêng và lưu reference:

// Trước (bị phình): sản phẩm với tất cả đánh giá nhúng trực tiếp
{
  _id: ObjectId("..."),
  name: "Widget",
  reviews: [ /* 5000 đánh giá */ ]
}

// Sau: collection riêng biệt
// products: { _id, name }
// reviews:  { _id, productId, text, rating, date }

Dùng $lookup khi cần join, hoặc truy vấn thẳng vào collection reviews khi render. Hai câu query là cái giá nhỏ để không bao giờ đụng đến giới hạn 16 MB.

Giải pháp 4 — Nén dữ liệu trước khi lưu

Đôi khi dữ liệu thực sự cần ở cùng nhau — một snapshot, một báo cáo đã serialize. Nén là giải pháp cuối cùng hợp lý. Payload JSON thường nén được 5–10 lần với gzip, có thể đưa object 50 MB xuống dưới 10 MB:

const zlib = require('zlib');

// Nén
const raw = JSON.stringify(bigObject);
const compressed = zlib.gzipSync(raw);  // trả về Buffer

await db.collection('snapshots').insertOne({
  _id: snapshotId,
  data: compressed,  // lưu dưới dạng BinData
  compressedAt: new Date()
});

// Giải nén khi đọc
const doc = await db.collection('snapshots').findOne({ _id: snapshotId });
const original = JSON.parse(zlib.gunzipSync(doc.data).toString());

Lưu ý quan trọng: engine WiredTiger của MongoDB có nén dữ liệu ở tầng lưu trữ, nhưng đó là trong suốt và không ảnh hưởng đến kích thước BSON document. Bạn cần nén ở tầng ứng dụng — như gzip ở trên — để giảm kích thước document trước khi insert.

Kiểm tra sau khi sửa

Sau khi áp dụng giải pháp, xác nhận document mới thực sự nằm trong giới hạn:

// Kiểm tra kích thước document mới
const newDoc = await db.collection('mycollection').findOne({ _id: newId });
const { BSON } = require('bson');
console.log('Kích thước (bytes):', BSON.calculateObjectSize(newDoc));
// Phải dưới 16777216

Với GridFS, xác minh file đã được lưu đúng cách:

// Trong mongosh
db.fs.files.findOne({ filename: 'report.pdf' });
// Phải hiển thị { length: ..., chunkSize: 261120, ... }

Bài học thực tế

  • Đừng bao giờ lưu file dưới dạng chuỗi Base64 trong document. Một file PDF 12 MB sau khi mã hóa sẽ thành ~16 MB — và bạn chưa thêm một trường metadata nào. Hãy dùng GridFS hoặc object store (S3, Cloudflare R2).
  • Thêm cảnh báo kích thước cho các collection ghi nhiều. Kiểm tra Object.bsonsize() trong staging với các document tăng trưởng theo thời gian. Phát hiện document 10 MB sớm rẻ hơn nhiều so với xử lý sự cố lúc 3 giờ sáng.
  • Giới hạn độ dài mảng ở tầng schema. Validator của Mongoose, kiểm tra ở tầng ứng dụng — cách nào cũng được. Đừng chờ MongoDB báo document quá lớn.
  • Giới hạn là trên từng document, không phải trên collection. Tách dữ liệu lớn thành nhiều document trong cùng một collection luôn an toàn.

Related Error Notes