エラーの内容
MongoServerError: Topology was destroyed
クエリ、インサート、またはアグリゲーションを実行した瞬間、結果の代わりにこのエラーが返されます。操作は一切実行されていません。MongoDBの内部接続トポロジーが、コードが使用しようとする前(またはその最中)に破棄されたことを意味します。
根本原因
MongoDBドライバーはトポロジー(アクティブな接続、サーバーの健全性、接続プールを追跡する内部オブジェクト)を維持しています。「Topology was destroyed」とは、コードがまだそのオブジェクトを参照している最中に、ドライバーがそれをシャットダウンしたことを意味します。
よくある原因:
client.close()(またはmongoose.disconnect())を呼び出した後にクエリを実行した — 典型的な非同期タイミングのバグ。- プロセスが
SIGINT/SIGTERMを受け取り、シャットダウンハンドラーが実行中のクエリが残っているうちに接続を閉じた。 serverSelectionTimeoutMSがタイムアウトした — タイムアウト時間内(デフォルト:30秒)に使用可能なサーバーが見つからず、再試行せずにトポロジーを破棄した。MongoClientが関数やリクエストハンドラー内で作成され、終了時にクローズされた — 次の呼び出しが古い閉じられたインスタンスを参照することがある。- 未処理のPromise拒否がアプリを操作途中でクラッシュさせ、アクティブな接続を突然切断した。
修正1 — クライアントを早期にクローズしない(最も一般的)
スクリプトやLambda形式の関数が最も多い原因です。awaitの漏れにより、クエリが開始する前にclient.close()が実行されます:
// BAD — client closes before findOne resolves
const client = new MongoClient(uri);
await client.connect();
client.db('mydb').collection('users').findOne({ id: 1 }); // no await!
await client.close(); // topology gone before findOne completes
// GOOD — await every operation before closing
const client = new MongoClient(uri);
try {
await client.connect();
const user = await client.db('mydb').collection('users').findOne({ id: 1 });
console.log(user);
} finally {
await client.close();
}
すべてのデータベース呼び出しにはawaitが必要です。.then()チェーンを使用している場合、client.close()は他の操作と並列ではなく、チェーンの最後に連結する必要があります。
修正2 — シャットダウン時の競合状態を修正する
長時間稼働するサーバーでは、プロセスがリクエスト処理中にシャットダウンするとこのエラーが発生します。単純なSIGTERMハンドラーはMongoDBを即座にクローズし、実行中のクエリを取り残します:
// BAD — closes DB while HTTP requests may still be running
process.on('SIGTERM', async () => {
await mongoose.disconnect();
process.exit(0);
});
// GOOD — drain HTTP first, then close DB
process.on('SIGTERM', async () => {
server.close(async () => { // stop accepting new HTTP requests
await mongoose.disconnect(); // only now close MongoDB
process.exit(0);
});
});
HTTPサーバーを停止 → アクティブな接続が完了するまで待つ → MongoDBをクローズ、という順序が正しいです。いずれかのステップを省略すると、実際のトラフィック下でこのエラーが発生します。
修正3 — serverSelectionTimeoutMSを増やす
高負荷時やネットワークが遅い場合、ドライバーが使用可能なサーバーを見つけるためにタイムアウトすることがあります。諦めた時点でトポロジーを破棄します。タイムアウト値を増やしましょう:
// Node.js — mongodb driver
const client = new MongoClient(uri, {
serverSelectionTimeoutMS: 10000, // bump from the default 30000 if failing fast
connectTimeoutMS: 10000,
socketTimeoutMS: 45000,
});
# mongoose
mongoose.connect(uri, {
serverSelectionTimeoutMS: 10000,
connectTimeoutMS: 10000,
socketTimeoutMS: 45000,
});
タイムアウトを調整する前に、MongoDBサーバーに実際に到達できるか確認してください:
mongosh "mongodb://user:pass@host:27017/dbname" --eval "db.adminCommand({ ping: 1 })"
pingが成功すれば、ネットワークの問題は完全に除外できます。
修正4 — MongoClientを再利用する(リクエストごとに作成しない)
リクエストごとに新しいMongoClientを作成してクローズするのは、意外と多いアンチパターンです。並行リクエストがある状況では、重複したリクエストが同じトポロジーを参照し、そのうちの1つがクローズしてしまいます:
// BAD — new client per request, race condition waiting to happen
app.get('/users', async (req, res) => {
const client = new MongoClient(uri);
await client.connect();
const users = await client.db('mydb').collection('users').find().toArray();
await client.close(); // next overlapping request may still reference this topology
res.json(users);
});
// GOOD — one client for the whole process
const client = new MongoClient(uri);
await client.connect();
app.get('/users', async (req, res) => {
const users = await client.db('mydb').collection('users').find().toArray();
res.json(users);
});
MongoDBの組み込み接続プール(デフォルトで100接続)がすべての並行処理を管理します。プロセスあたり1つのMongoClientが正しい設計です。
修正5 — 重要な操作にリトライロジックを追加する
ネットワークの一時的な問題は避けられません。重要なクエリをリトライロジックでラップすることで、一時的なトポロジーエラーがユーザーに表示されるのを防げます:
async function withRetry(fn, maxRetries = 3) {
for (let attempt = 1; attempt setTimeout(r, 1000 * attempt)); // 1s, 2s, 3s backoff
continue;
}
throw err;
}
}
}
// Usage
const result = await withRetry(() =>
db.collection('orders').findOne({ _id: orderId })
);
指数バックオフ(1秒、2秒、3秒)により、過負荷のサーバーに過度なリクエストを送らずに、ドライバーが回復する時間を確保できます。
修正6 — pymongo(Python)
Pythonでは異なる形で表れます — 通常はpymongo.errors.InvalidOperation: Cannot use MongoClient after close — ですが根本原因は同じです。コンテキストマネージャーを使用して修正します:
from pymongo import MongoClient
# BAD — client closed before find_one runs
client = MongoClient(uri)
db = client['mydb']
client.close()
db.users.find_one() # topology already destroyed
# GOOD — context manager closes the client only after the block exits
with MongoClient(uri) as client:
db = client['mydb']
result = db.users.find_one({'id': 1})
print(result)
修正の確認
再起動して祈るだけではいけません。エラーが実際に解消されたか確認しましょう:
- 以前失敗していたコードパスを実行してログを確認 —
Topology was destroyedのエントリーが表示されないこと。 - MongoDB Atlasのメトリクスまたは直接コマンドで接続プールの健全性を確認:
db.serverStatus().connections
// { current: 5, available: 195, totalCreated: 12 }
autocannonまたはk6で負荷テストを実行し(ほとんどのAPIには同時50〜200ユーザーが適切)、すべてのリクエストでトポロジーエラーがゼロであることを確認する。
予防策
- プロセスあたり1クライアント — 起動時に
MongoClientを1回だけ初期化し、アプリ全体で共有する。 - グレースフルシャットダウンの順序 — HTTPの接続を先にドレインし、その後で
mongoose.disconnect()またはclient.close()を呼び出す。逆の順序にしてはいけない。 - すべてにawaitを付ける — MongoDBの操作は
awaitまたは適切な.then()チェーンなしで実行してはいけない。リンタールール(TypeScriptのno-floating-promises)でこれを自動的に検出できる。 - 実態に合わせてタイムアウトを調整する — MongoDB Atlasクラスターが
us-east-1にあり、アプリがap-southeast-1で動作している場合、serverSelectionTimeoutMSを5秒にすると問題が発生する。まず実際のラウンドトリップレイテンシーを計測すること。 - 接続プールを監視する —
connections.availableが20を下回ったらアラートを発する。プールの枯渇が、高負荷時のトポロジー破壊の引き金になることが多い。

