Fix MongoPoolClosedError: Attempted to check out a connection from closed connection pool

intermediate🍃 MongoDB2026-05-10| Node.js 16+, MongoDB Node.js Driver 4.x/5.x, Mongoose 6.x/7.x, chạy trên Linux/macOS/Windows

Error Message

MongoPoolClosedError: Attempted to check out a connection from closed connection pool
#mongodb#connection-pool#mongoose#nodejs#graceful-shutdown

Chuyện gì đang xảy ra

Đâu đó trong code của bạn, client.close() hoặc mongoose.disconnect() đã được gọi. Điều đó đã làm cạn kiệt và đóng connection pool. Vấn đề là? Một request đang chờ xử lý, một async callback, hoặc một background job vẫn cố thực hiện thao tác MongoDB sau đó — và va phải một cánh cửa đã đóng chặt.

Toàn bộ lỗi trông như thế này:

MongoPoolClosedError: Attempted to check out a connection from closed connection pool
    at ConnectionPool.checkOut (/node_modules/mongodb/lib/cmap/connection_pool.js:...)
    at Server.command (/node_modules/mongodb/lib/sdam/server.js:...)

Đây không phải là lỗi mạng thoáng qua hay replica set failover. Pool đã bị đóng có chủ đích — thường là trong quá trình tắt ứng dụng — trong khi các thao tác đang thực thi vẫn còn đang chạy. Race condition kinh điển.

Bước 1: Tìm chỗ pool bị đóng quá sớm

Đầu tiên, xác nhận thời điểm xảy ra bằng một log đơn giản:

// Native driver
client.on('connectionPoolClosed', () => {
  console.log('[MongoDB] Connection pool closed at', new Date().toISOString());
});

// Mongoose
mongoose.connection.on('disconnected', () => {
  console.log('[Mongoose] Disconnected at', new Date().toISOString());
});

Kích hoạt lệnh tắt (SIGTERM, SIGINT, hoặc cách bạn thường dừng tiến trình) rồi so sánh các mốc thời gian. Nếu pool đóng trước thao tác DB cuối cùng của bạn, bạn đang gặp race condition. Trong hầu hết ứng dụng, khoảng cách này dưới 200ms — đủ để một findOne() chậm lọt qua.

Bước 2: Kiểm tra shutdown handler của bạn

Chín trong mười trường hợp, thủ phạm trông như thế này:

// XẤU — đóng pool ngay lập tức, gây race với các request đang xử lý
process.on('SIGTERM', () => {
  mongoose.disconnect(); // không có await — bắn rồi quên
  server.close();
  process.exit(0);
});

Bất kỳ request nào đã nằm bên trong một async pipeline — một query chưa được await — sẽ đụng phải pool đã đóng và ném ra MongoPoolClosedError. Dưới tải cao, điều này xảy ra gần như mỗi lần deploy.

Bước 3: Sửa thứ tự shutdown

Dừng nhận công việc mới trước. Để công việc hiện tại hoàn thành. Đóng MongoDB sau cùng.

// TỐT — shutdown có thứ tự, graceful
async function shutdown(signal) {
  console.log(`Received ${signal}, shutting down...`);

  // 1. Dừng nhận HTTP request mới
  await new Promise((resolve) => server.close(resolve));
  console.log('HTTP server closed');

  // 2. Tất cả HTTP handler đã xong — an toàn để ngắt kết nối
  await mongoose.disconnect();
  console.log('MongoDB disconnected');

  process.exit(0);
}

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT',  () => shutdown('SIGINT'));

server.close() ngăn kết nối mới vào, nhưng cho phép các request đang hoạt động hoàn thành. Chỉ sau khi callback của nó được gọi bạn mới ngắt kết nối MongoDB. Không có request nào đang xử lý có thể chạm tới pool đã đóng.

Bước 4: Thêm timeout guard

Một request bị treo có thể chặn quá trình shutdown mãi mãi. Giới hạn cứng 10 giây sẽ ngăn điều đó:

async function shutdown(signal) {
  const TIMEOUT_MS = 10_000; // thoát sau 10 giây

  const forceExit = setTimeout(() => {
    console.error('Shutdown timed out, forcing exit');
    process.exit(1);
  }, TIMEOUT_MS);

  forceExit.unref(); // không giữ event loop sống chỉ vì timer này

  try {
    await new Promise((resolve) => server.close(resolve));
    await mongoose.disconnect();
    console.log('Graceful shutdown complete');
    process.exit(0);
  } catch (err) {
    console.error('Error during shutdown:', err);
    process.exit(1);
  }
}

10 giây đủ để xử lý phần lớn các query chậm. Giảm xuống 5 giây cho các service nhạy cảm về độ trễ, hoặc tăng lên 30 giây nếu các job của bạn thực sự chạy lâu.

Bước 5: Xử lý background job riêng biệt

Các cron job, queue consumer, và vòng lặp setInterval vô hình với server.close(). Chúng có thể thực hiện các truy vấn MongoDB sau khi HTTP server đã dừng. Hãy theo dõi chúng một cách tường minh:

const activeJobs = new Set();

function runJob(fn) {
  const job = fn().finally(() => activeJobs.delete(job));
  activeJobs.add(job);
  return job;
}

async function shutdown(signal) {
  // Dừng lên lịch job mới (clearInterval, queue.close(), v.v.)

  // Chờ các job đang chạy hoàn thành
  if (activeJobs.size > 0) {
    console.log(`Waiting for ${activeJobs.size} background job(s)...`);
    await Promise.allSettled([...activeJobs]);
  }

  await new Promise((resolve) => server.close(resolve));
  await mongoose.disconnect();
  process.exit(0);
}

Bọc các job trong runJob() giúp bạn có số lượng job đang chạy theo thời gian thực. Bạn sẽ thấy các dòng log như Waiting for 3 background job(s)... thay vì các crash im lặng.

Bước 6: Native MongoDB driver

Quy tắc tương tự áp dụng. Gọi client.close() sau cùng:

const { MongoClient } = require('mongodb');
const client = new MongoClient(process.env.MONGODB_URI);

async function shutdown() {
  await new Promise((resolve) => server.close(resolve));
  await client.close();
  console.log('MongoDB client closed');
  process.exit(0);
}

Lưu ý nhanh về tham số boolean force: client.close(false) (mặc định) cho phép pool thoát cạn dần một cách graceful. client.close(true) đóng ngay lập tức — điều này vẫn có thể ném lỗi này với các thao tác thực sự đang trong flight. Mặc định hầu như luôn là lựa chọn bạn muốn.

Kiểm tra lại

Khởi động lại ứng dụng, gửi một số traffic, rồi kích hoạt shutdown trong khi các request đang xử lý:

# Terminal 1 — bắn liên tục request
while true; do curl -s http://localhost:3000/api/data > /dev/null; done

# Terminal 2 — kích hoạt shutdown sau 5 giây
sleep 5 && kill -SIGTERM $(pgrep -f 'node app.js')

Theo dõi log. Bạn muốn thấy: HTTP server đóng → MongoDB ngắt kết nối → exit code 0. Không có MongoPoolClosedError. Nếu đó là những gì bạn thấy, bạn đã xong.

Bài học rút ra

  • Thứ tự quan trọng hơn bạn nghĩ. MongoDB luôn phải là thứ cuối cùng bạn đóng — sau khi HTTP server và job queue đã được thoát cạn.
  • Không bao giờ bắn rồi quên disconnect(). Luôn await nó. Nếu không, bạn không biết khi nào (hoặc liệu) pool có thực sự đóng hay không.
  • Background job sẽ làm bạn khổ. Bất kỳ setInterval hay queue consumer nào đụng tới MongoDB đều cần được dừng và thoát cạn trước khi bạn đóng pool.
  • Timeout cứu các lần deploy. Một graceful shutdown có thể treo mãi mãi còn tệ hơn là một cái hết timeout — ít nhất process manager (systemd, Docker, PM2) có thể khởi động lại nó.

Related Error Notes