Sửa lỗi 'MaxListenersExceededWarning: Possible EventEmitter Memory Leak Detected' trong Node.js

intermediate💚 Node.js2026-04-15| Node.js (tất cả phiên bản), Linux / macOS / Windows — mọi dự án sử dụng EventEmitter, streams, HTTP server, hoặc thư viện như socket.io, fs.watch, readline

Error Message

(node:1234) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 listeners added. Use emitter.setMaxListeners() to increase limit
#Node.js#EventEmitter#Memory Leak#Events

TL;DR — Sửa Nhanh

Cảnh báo này xuất hiện khi có hơn 10 listener được gắn vào cùng một event trên một EventEmitter. Node.js coi 10 là ngưỡng an toàn mặc định để phát hiện rò rỉ bộ nhớ không chủ ý.

Cách xử lý phụ thuộc vào lý do bạn có nhiều listener đến vậy:

  • Bạn cố ý cần nhiều listener hơn → tăng giới hạn bằng emitter.setMaxListeners(n)
  • Bạn đang thêm listener bên trong vòng lặp hoặc mỗi request → đây là rò rỉ thật sự — hãy sửa code, đừng chỉ tăng giới hạn

Nguyên Nhân Gây Ra Cảnh Báo

Thông báo chính xác bạn sẽ thấy:

(node:1234) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 listeners added. Use emitter.setMaxListeners() to increase limit

Node.js ghi lại cảnh báo này khi một emitter đơn lẻ có nhiều hơn defaultMaxListeners (mặc định: 10) listener đã đăng ký cho cùng một event. Các nguyên nhân thường gặp:

  • Đăng ký listener trên mỗi HTTP request mà không bao giờ xóa đi
  • Gọi emitter.on() bên trong một hàm chạy lặp đi lặp lại — ví dụ, một middleware được kích hoạt trên mỗi route
  • Sử dụng thư viện (readline, socket.io, chokidar) đăng ký listener nội bộ, trong khi bạn tạo instance mới trên mỗi request
  • Quên gọi removeListener() hoặc off() khi dọn dẹp

Bước 1 — Tìm Emitter Nào Đang Bị Rò Rỉ

Chạy ứng dụng và đọc stack trace. Node.js 10+ tự động in ra:

(node:1234) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 listeners added to [EventEmitter].
  at EventEmitter.addListener (node:events:596:17)
  at Server.<anonymous> (/app/server.js:42:10)   <-- file của bạn
  ...

Tên file và số dòng đó chính là nơi listener đang được thêm vào. Mở file ra và kiểm tra xem nó có nằm bên trong callback, vòng lặp, hay request handler không.

Không chắc hiện tại có bao nhiêu listener? In ra kiểm tra:

console.log(emitter.listenerCount('data')); // có bao nhiêu listener 'data'?

Bước 2 — Sửa Rò Rỉ Thật Sự (Đa Số Trường Hợp)

Hầu hết thời gian, vấn đề rất đơn giản: bạn đang gắn listener bên trong một hàm chạy lặp đi lặp lại, và không có gì xóa nó đi. Hãy chuyển việc đăng ký ra ngoài hàm đó — hoặc xóa listener sau khi dùng xong.

Pattern sai — listener được thêm vào mỗi lần gọi

// server.js
app.get('/stream', (req, res) => {
  // BUG: thêm listener mới trên mỗi request, không bao giờ xóa
  process.on('exit', () => res.end());
});

Sau 11 request, cảnh báo xuất hiện. Sau hàng nghìn request, bạn có rò rỉ bộ nhớ thật sự.

Đã sửa — xóa listener khi xong

app.get('/stream', (req, res) => {
  const cleanup = () => res.end();
  process.once('exit', cleanup); // 'once' tự xóa sau khi kích hoạt

  res.on('close', () => {
    process.removeListener('exit', cleanup); // dọn dẹp tường minh nếu client ngắt kết nối trước
  });
});

Dùng once() thay vì on() cho các event một lần

// on() giữ listener tồn tại mãi mãi
emitter.on('finish', handleFinish);

// once() tự xóa sau khi kích hoạt — không cần dọn dẹp thủ công
emitter.once('finish', handleFinish);

Thay đổi đơn giản này giải quyết phần lớn các báo cáo rò rỉ listener trong ứng dụng Express và Koa.

Tạo instance mới trên mỗi request (thường gặp với readline, socket.io)

// SAI: tạo readline interface mới trên mỗi request
app.post('/process', (req, res) => {
  const rl = readline.createInterface({ input: req });
  rl.on('line', processLine);
  // rl không bao giờ được đóng — listener tích lũy theo từng request
});

// ĐÚNG: đóng khi xong
app.post('/process', (req, res) => {
  const rl = readline.createInterface({ input: req });
  rl.on('line', processLine);
  rl.on('close', () => res.json({ ok: true }));
  req.on('end', () => rl.close()); // đảm bảo dọn dẹp
});

Bước 3 — Tăng Giới Hạn Chỉ Khi Thực Sự Cần Nhiều Listener Hơn

Đôi khi tất cả listener đều hợp lệ — một kênh pub/sub với 20 subscriber, hoặc một event bus dùng chung trong hệ thống plugin. Trong những trường hợp đó, hãy tăng giới hạn trên emitter cụ thể đó:

const EventEmitter = require('events');
const emitter = new EventEmitter();

// Cho phép tối đa 30 listener trên emitter này
emitter.setMaxListeners(30);

emitter.on('message', handler1);
emitter.on('message', handler2);
// ... lên đến 30 một cách an toàn

Cũng có tùy chọn toàn cục — nhưng hãy dùng cẩn thận:

const EventEmitter = require('events');
EventEmitter.defaultMaxListeners = 20;

Cảnh báo: tăng giá trị mặc định toàn cục sẽ che giấu các rò rỉ thật sự ở nơi khác trong cùng tiến trình. Hãy dùng setMaxListeners() cho từng emitter riêng lẻ khi có thể.

Bước 4 — Tắt Cảnh Báo Cho Code Bên Thứ Ba Đã Biết Là An Toàn

Một số thư viện — AWS SDK, một số phần nội bộ của socket.io — kích hoạt cảnh báo ngay cả khi chúng dọn dẹp đúng cách. Bạn có thể tắt cảnh báo trên một emitter cụ thể mà không cần thay đổi logic giới hạn:

// 0 = không giới hạn listener, tắt cảnh báo
emitter.setMaxListeners(0);

Chỉ dùng cách này khi bạn chắc chắn về những gì đang đăng ký listener và tại sao. Đây là nút tắt tiếng, không phải giải pháp thật sự.

Xác Nhận — Kiểm Tra Bản Sửa Đã Hoạt Động

  • Khởi động lại ứng dụng và theo dõi console — cảnh báo phải biến mất.
  • Tái hiện lại luồng code đã kích hoạt cảnh báo: mô phỏng tải, gửi nhiều request, stress-test route.
  • Thêm kiểm tra tạm thời trong quá trình phát triển:
setInterval(() => {
  const count = emitter.listenerCount('data');
  if (count > 5) console.warn(`High listener count on 'data': ${count}`);
}, 5000);
  • Trên môi trường production, bắt cảnh báo theo chương trình và gửi đến hệ thống giám sát của bạn:
process.on('warning', (warning) => {
  if (warning.name === 'MaxListenersExceededWarning') {
    console.error('Listener leak detected:', warning.message, warning.stack);
    // chuyển tiếp đến Datadog, Sentry, v.v.
  }
});

Tóm Tắt

  • Cảnh báo này có nghĩa là có 11+ listener được đăng ký cho một event trên một emitter
  • 99% trường hợp đây là lỗi thật — listener được thêm bên trong vòng lặp hoặc per-request handler mà không có dọn dẹp
  • Dùng once() cho các event một lần, removeListener() / off() để dọn dẹp tường minh, và luôn đóng stream và interface khi dùng xong
  • Chỉ gọi setMaxListeners(n) khi bạn thực sự cần nhiều listener một cách có chủ ý — không phải để làm cho cảnh báo biến mất

Related Error Notes