结论:mongoose内存泄漏90%源于未显式调用mongoose.disconnect(),导致连接池、心跳定时器等堆外资源滞留;须在测试afterall、进程退出前await断开,并避免与原生mongoclient混用。

直接说结论:Mongoose引发的内存泄漏,90%不是Mongoose本身的问题,而是mongoose.connect()后没调用mongoose.disconnect(),或在测试/脚本中重复连接未清理,导致连接池、监听器、定时器持续滞留。
为什么mongoose.disconnect()必须显式调用
Mongoose内部封装了MongoClient,但它的连接管理是“懒关闭”——不主动断开,就不会释放底层TCP连接池、心跳定时器(topology.heartbeatFrequencyMS)、服务器发现任务。这些后台任务会持续持有对Buffer、EventEmitter、甚至整个model实例的引用,阻止GC回收。
常见错误现象包括:
MaxListenersExceededWarning: Possible EventEmitter memory leak detected- 进程
rss缓慢上涨,heapUsed相对平稳(说明泄漏在堆外) - 开发时反复
Ctrl+C重启,lsof -i :27017能看到残留的ESTABLISHED连接
测试脚本中漏掉mongoose.disconnect()是最隐蔽的泄漏源
Jest/Mocha等测试框架默认并行执行test suite,每个beforeAll()里调用mongoose.connect(),但若afterAll()里没await mongoose.disconnect(),一次跑10个suite就会留下10个未关闭的连接池。
实操建议:
根据 package.json 中的 engines 字段自动检测 Node.js 版本需求,未安装 nvm 时自动安装并切换至对应版本。支持 Windows、macOS、Linux。适用于 Node.js 项目开发、版本切换和环境配置。
- 所有测试文件末尾统一加:
afterAll(async () => { await mongoose.disconnect(); }); - 避免在
beforeEach()里重复connect();如需隔离数据,用mongoose.connection.db.dropDatabase()代替重连 - 检查
jest.config.js是否启用了--runInBand:未启用时,多个worker会各自创建独立连接,加剧泄漏
混合使用mongoose和原生mongodb驱动导致双连接池
有些项目在迁移过程中同时存在mongoose.connect()和new MongoClient().connect(),两者互不感知。关掉mongoose.disconnect()只清了Mongoose的连接池,client.close()又只关了原生驱动的——另一个还在后台心跳。
识别方法:
- 启动后执行
console.log(mongoose.connections.map(c => c.readyState)),确认只有1个1(connected) - 搜索代码中所有
require('mongodb')和new MongoClient,确认是否与Mongoose共用同一URI和选项 - 若必须共存,确保只让一个负责连接,另一个用
useDb()复用已有连接
优雅退出时disconnect未await完成就exit
Express应用中常这样写信号处理:
process.on('SIGINT', () => { mongoose.disconnect(); process.exit(0); });
这会导致disconnect()被调用但未等待完成,连接池和定时器仍在运行。正确做法是:
- 用
async包装,并await后再exit: process.on('SIGINT', async () => { console.log('Closing Mongoose connection...'); await mongoose.disconnect(); process.exit(0); });- 在Kubernetes环境,还要配合
preStophook,确保容器终止前有足够时间(至少10秒)执行disconnect
最容易被忽略的是:mongoose.disconnect()在连接本就断开时会resolve空操作,但若它被多次调用(比如信号被触发两次),第二次会抛NotConnectedError——所以加个if (mongoose.connection.readyState === 1)判断更稳妥。










