何が起きたか
ドキュメントを挿入または更新しようとしたとき、MongoDBが次のエラーをスローしました:
MongoServerError: document is larger than the maximum size 16777216
この数値 — 16,777,216バイト — はちょうど16MBです。これはMongoDBのBSONドキュメントサイズのハード制限です。レプリカセット、Atlas、ローカル開発環境など、どこで実行しても関係ありません。この上限はBSON仕様自体に組み込まれており、設定で変更することはできません。
よくある原因:
- Base64エンコードされた画像やPDFをドキュメントフィールドに直接保存している — Base64はバイナリデータを約33%膨張させるため、12MBのPDFはメタデータフィールドを1つも追加しないうちに制限に達してしまう
$pushのたびに際限なく増え続けるアレイ(ログ、イベント、履歴など)がついに上限を超えてしまう- ORMから大きなオブジェクトグラフをシリアライズして一度に挿入している
- 誰も追跡していなかった数ヶ月分の蓄積データが、本番環境で問題を起こすまで気づかれなかった
修正前に計測する
どのフィールドが肥大化しているか推測するのではなく、実際に計測しましょう。mongoshの場合:
// mongoshの場合
const doc = db.mycollection.findOne({ _id: ObjectId("...") });
Object.bsonsize(doc);
// 例: 18432000 ← 16MBを超えている
Node.jsのネイティブドライバの場合:
const { BSON } = require('bson');
const size = BSON.calculateObjectSize(doc);
console.log(`ドキュメントサイズ: ${(size / 1024 / 1024).toFixed(2)} MB`);
Python(PyMongo)の場合:
import bson
size = len(bson.encode(doc))
print(f"ドキュメントサイズ: {size / 1024 / 1024:.2f} MB")
肥大化したフィールド(通常はバイナリブロブや制御不能なアレイ)を特定したら、状況に合った以下の修正方法を選んでください。
修正1 — バイナリ/ファイルデータにはGridFSを使う
ファイル、画像、PDFをドキュメントフィールドに直接保存するのは間違ったアプローチです。GridFSはまさにこのために作られました。ファイルを255KBのチャンクに分割してメタデータを別に保存し、16MBの制限を完全に回避します。
Node.jsの例(ネイティブドライバ):
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:', uploadStream.id);
});
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_id}')
返されたfile_idのみをメインドキュメントに保存してください。取得する際はbucket.openDownloadStream(file_id)またはfs.get(file_id)を使います。
修正2 — ドキュメントを分割する(バケットパターン)
制御不能なアレイはよくある原因です。バケットパターンは各ドキュメントをN件に制限し、上限に達したら新しいドキュメントを作成します — イベントログ、テレメトリ、時系列データに対して確立されたアプローチです。
// 100kのログエントリを持つ1つのドキュメントの代わりに:
// { _id, userId, events: [ ...100000 items... ] }
// バケット化されたドキュメントを使用する:
// { _id, userId, bucket: 1, count: 200, events: [ ...200 items... ] }
// { _id, userId, bucket: 2, count: 200, events: [ ...200 items... ] }
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 }
);
各ドキュメントは余裕を持って16MB未満に収まります。イベントの範囲クエリも高速化されます。巨大な1つのドキュメントではなく、小さくて境界のあるドキュメントをスキャンするからです。
修正3 — 埋め込みではなく参照を使う
サブドキュメントの埋め込みは、小さくて安定したデータには最適です。しかし、時間とともに増えるデータ(レビュー、コメント、監査ログなど)には、埋め込みが負担になります。増え続けるデータを別のコレクションに移動し、参照を保存しましょう:
// 変更前(肥大化): すべてのレビューを埋め込んだ商品ドキュメント
{
_id: ObjectId("..."),
name: "Widget",
reviews: [ /* 5000件のレビュー */ ]
}
// 変更後: 別コレクションに分離
// products: { _id, name }
// reviews: { _id, productId, text, rating, date }
結合が必要な場合は$lookupを使うか、レンダリング時にレビューコレクションを直接クエリしてください。クエリが2回になっても、16MBの壁に当たらないことを考えれば安いコストです。
修正4 — 保存前に圧縮する
データが本当に一緒に属している場合(スナップショット、シリアライズされたレポートなど)もあります。圧縮は合理的な最終手段です。JSONペイロードはgzipで通常5〜10倍に圧縮でき、50MBのオブジェクトを10MB未満にできます:
const zlib = require('zlib');
// 圧縮
const raw = JSON.stringify(bigObject);
const compressed = zlib.gzipSync(raw); // Bufferを返す
await db.collection('snapshots').insertOne({
_id: snapshotId,
data: compressed, // BinDataとして保存
compressedAt: new Date()
});
// 読み取り時に解凍
const doc = await db.collection('snapshots').findOne({ _id: snapshotId });
const original = JSON.parse(zlib.gunzipSync(doc.data).toString());
注意点として、MongoDBのWiredTigerエンジンはストレージ層でデータを圧縮しますが、それは透過的でBSONドキュメントサイズには影響しません。挿入前にドキュメントサイズを削減するには、上記のgzipのようなアプリケーションレベルの圧縮が必要です。
修正を確認する
選択した修正を適用したら、新しいドキュメントが実際に制限内に収まっているか確認しましょう:
// 新しいドキュメントのサイズを確認する
const newDoc = await db.collection('mycollection').findOne({ _id: newId });
const { BSON } = require('bson');
console.log('サイズ(バイト):', BSON.calculateObjectSize(newDoc));
// 16777216を大幅に下回っているはず
GridFSの場合、ファイルが正しく保存されたか確認してください:
// mongoshの場合
db.fs.files.findOne({ filename: 'report.pdf' });
// { length: ..., chunkSize: 261120, ... } が表示されるはず
現場からの教訓
- ファイルをBase64文字列としてドキュメントに保存しないでください。 12MBのPDFはエンコードすると約16MBになります — まだメタデータフィールドを1つも追加していない段階で。GridFSまたはオブジェクトストア(S3、Cloudflare R2)を使用してください。
- 書き込みが多いコレクションにはサイズアラートを追加してください。 時間とともに増えるドキュメントに対して、ステージング環境で
Object.bsonsize()を確認してください。早期に発見した10MBのドキュメントは、深夜3時のインシデントよりはるかに安上がりです。 - スキーマレベルでアレイの長さを強制してください。 Mongooseのバリデータ、アプリケーション層のチェック — どちらでも機能します。MongoDBにドキュメントが大きすぎると言われるまで待たないでください。
- 制限はドキュメントごとであり、コレクションごとではありません。 同じコレクション内の複数のドキュメントに大きなデータを分割するのは常に安全です。

