Fix MongoServerError: Topology was destroyed trong MongoDB

intermediate🍃 MongoDB2026-03-22| Node.js (mongoose / mongodb driver), Python (pymongo), mọi MongoDB client — MongoDB 4.x–7.x

Error Message

MongoServerError: Topology was destroyed
#mongodb#topology#connection-pool#reconnect#driver

Lỗi Gặp Phải

MongoServerError: Topology was destroyed

Bạn thực hiện một câu truy vấn, thao tác insert, hoặc aggregation — nhưng thay vì nhận được kết quả, bạn nhận được lỗi này. Thao tác chưa hề được thực thi. Topology kết nối nội bộ của MongoDB đã bị hủy trước (hoặc trong khi) code của bạn cố sử dụng nó.

Nguyên Nhân

MongoDB driver duy trì một topology: một đối tượng nội bộ theo dõi các kết nối đang hoạt động, tình trạng server và connection pool. Lỗi "Topology was destroyed" có nghĩa là driver đã đóng đối tượng đó trong khi code của bạn vẫn đang trỏ vào nó.

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

  • Bạn đã gọi client.close() (hoặc mongoose.disconnect()) rồi sau đó tiếp tục chạy truy vấn — lỗi timing async điển hình.
  • Tiến trình nhận tín hiệu SIGINT / SIGTERM và shutdown handler đóng kết nối trong khi các truy vấn đang thực thi vẫn còn đang chờ.
  • serverSelectionTimeoutMS hết hạn — driver không thể tìm thấy server khả dụng trong khoảng thời gian timeout (mặc định: 30 giây) và hủy topology thay vì thử lại.
  • Một MongoClient được tạo bên trong một hàm hoặc request handler rồi bị đóng ở cuối — lần gọi tiếp theo đôi khi vẫn tham chiếu đến instance cũ đã bị đóng.
  • Một unhandled promise rejection làm crash ứng dụng giữa chừng, khiến các kết nối đang hoạt động bị đóng đột ngột.

Cách Sửa 1 — Không Đóng Client Quá Sớm (Phổ Biến Nhất)

Script và các hàm kiểu Lambda là thủ phạm lớn nhất ở đây. Thiếu await khiến client.close() chạy trước khi truy vấn kịp bắt đầu:

// SAI — client đóng trước khi findOne hoàn thành
const client = new MongoClient(uri);
await client.connect();
client.db('mydb').collection('users').findOne({ id: 1 }); // thiếu await!
await client.close(); // topology biến mất trước khi findOne hoàn tất

// ĐÚNG — await mọi thao tác trước khi đóng
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();
}

Mọi lời gọi đến database đều cần có await. Nếu bạn dùng chuỗi .then(), thì client.close() phải được nối vào cuối cùng — không phải chạy song song với các thao tác khác.

Cách Sửa 2 — Khắc Phục Race Condition Khi Shutdown

Các server chạy lâu dài gặp lỗi này khi tiến trình tắt giữa chừng một request. Một handler SIGTERM đơn giản đóng MongoDB ngay lập tức, bỏ rơi các truy vấn đang thực thi:

// SAI — đóng DB trong khi các HTTP request vẫn có thể đang chạy
process.on('SIGTERM', async () => {
  await mongoose.disconnect();
  process.exit(0);
});

// ĐÚNG — drain HTTP trước, sau đó mới đóng DB
process.on('SIGTERM', async () => {
  server.close(async () => {        // dừng nhận HTTP request mới
    await mongoose.disconnect();    // lúc này mới đóng MongoDB
    process.exit(0);
  });
});

Dừng HTTP server → chờ các kết nối đang hoạt động hoàn tất → đóng MongoDB. Bỏ qua bất kỳ bước nào sẽ gây ra lỗi này trong điều kiện traffic thực tế.

Cách Sửa 3 — Tăng serverSelectionTimeoutMS

Khi tải cao hoặc mạng chậm, driver có thể timeout khi tìm kiếm server khả dụng. Khi bỏ cuộc, nó hủy topology. Hãy tăng giá trị timeout:

// Node.js — mongodb driver
const client = new MongoClient(uri, {
  serverSelectionTimeoutMS: 10000,  // tăng từ giá trị mặc định 30000 nếu fail nhanh
  connectTimeoutMS: 10000,
  socketTimeoutMS: 45000,
});

# mongoose
mongoose.connect(uri, {
  serverSelectionTimeoutMS: 10000,
  connectTimeoutMS: 10000,
  socketTimeoutMS: 45000,
});

Trước khi điều chỉnh timeout, hãy xác minh MongoDB server có thực sự truy cập được không:

mongosh "mongodb://user:pass@host:27017/dbname" --eval "db.adminCommand({ ping: 1 })"

Ping thành công sẽ loại trừ hoàn toàn các vấn đề về mạng.

Cách Sửa 4 — Tái Sử Dụng Một MongoClient (Không Tạo Mới Mỗi Request)

Tạo một MongoClient mới cho mỗi request — rồi đóng nó — là một anti-pattern khá phổ biến. Trong điều kiện có concurrency, các request chồng lên nhau sẽ dùng chung một topology mà một trong số chúng vừa đóng:

// SAI — client mới cho mỗi request, race condition chực chờ xảy ra
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(); // request chồng lên tiếp theo vẫn có thể đang tham chiếu topology này
  res.json(users);
});

// ĐÚNG — một client cho toàn bộ tiến trình
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);
});

Connection pool tích hợp của MongoDB (mặc định 100 kết nối) xử lý toàn bộ concurrency cho bạn. Một MongoClient cho mỗi tiến trình là mô hình đúng đắn.

Cách Sửa 5 — Thêm Logic Retry Cho Các Thao Tác Quan Trọng

Sự cố mạng thoáng qua là điều không tránh khỏi. Bọc các truy vấn quan trọng trong logic retry giúp ngăn lỗi topology tạm thời hiển thị ra cho người dùng:

async function withRetry(fn, maxRetries = 3) {
  for (let attempt = 1; attempt  setTimeout(r, 1000 * attempt)); // backoff 1s, 2s, 3s
        continue;
      }
      throw err;
    }
  }
}

// Cách dùng
const result = await withRetry(() =>
  db.collection('orders').findOne({ _id: orderId })
);

Backoff theo cấp số nhân (1 giây, 2 giây, 3 giây) cho driver thời gian phục hồi mà không liên tục tấn công server đang gặp sự cố.

Cách Sửa 6 — pymongo (Python)

Python biểu hiện lỗi này khác đi — thường là pymongo.errors.InvalidOperation: Cannot use MongoClient after close — nhưng nguyên nhân gốc rễ giống hệt nhau. Cách sửa là dùng context manager:

from pymongo import MongoClient

# SAI — client đóng trước khi find_one chạy
client = MongoClient(uri)
db = client['mydb']
client.close()
db.users.find_one()  # topology đã bị hủy

# ĐÚNG — context manager đóng client chỉ sau khi khối lệnh kết thúc
with MongoClient(uri) as client:
    db = client['mydb']
    result = db.users.find_one({'id': 1})
    print(result)

Xác Nhận Đã Sửa Xong

Đừng chỉ restart rồi hy vọng mọi thứ ổn. Hãy xác nhận lỗi thực sự đã biến mất:

  • Kích hoạt lại đoạn code trước đây gặp lỗi và kiểm tra log — không còn mục Topology was destroyed nào nữa.
  • Kiểm tra tình trạng connection pool trong MongoDB Atlas metrics hoặc trực tiếp:
db.serverStatus().connections
// { current: 5, available: 195, totalCreated: 12 }

  • Chạy load test với autocannon hoặc k6 (50–200 concurrent user là phù hợp cho hầu hết API) và xác nhận không có lỗi topology nào trong toàn bộ các request.

Phòng Ngừa

  • Một client cho mỗi tiến trình — khởi tạo MongoClient một lần khi khởi động, dùng chung trong toàn bộ ứng dụng.
  • Thứ tự shutdown đúng chuẩn — drain HTTP connection trước, sau đó mới gọi mongoose.disconnect() hoặc client.close(). Không bao giờ làm ngược lại.
  • Await mọi thứ — không có thao tác MongoDB nào được thực thi mà thiếu await hoặc chuỗi .then() đầy đủ. Một quy tắc linter (no-floating-promises trong TypeScript) sẽ tự động phát hiện điều này.
  • Điều chỉnh timeout phù hợp thực tế — nếu MongoDB Atlas cluster của bạn ở us-east-1 nhưng ứng dụng chạy ở ap-southeast-1, giá trị serverSelectionTimeoutMS là 5 giây sẽ gây hại cho bạn. Hãy đo latency round-trip thực tế trước.
  • Theo dõi connection pool — cảnh báo khi connections.available giảm xuống dưới 20. Pool exhaustion thường chính là nguyên nhân kích hoạt việc hủy topology khi tải cao.

Related Error Notes