Next 16.3.0 emit MaxListenersExceededWarning #96973
Replies: 4 comments 6 replies
|
Confirming this reproduces on our side as well, after upgrading an app from 15.5.21 → 16.3.0. Identical warning, identical 448 occurrences in ~17 minutes on the new deployment; 0 on the previous Next 15 deployment, no other changes. I dug into it a bit and have some data that might help. TL;DR: it is not a leak. Next's per-request 1. It is not a leak, and the occurrence count proves itNode's const { EventEmitter } = require('events')
let warnings = 0
process.on('warning', (w) => { if (w.name === 'MaxListenersExceededWarning') warnings++ })
const a = new EventEmitter()
for (let i = 0; i < 30; i++) a.on('close', () => {})
// → 1 warning (not 20)
const b = new EventEmitter()
for (let i = 0; i < 15; i++) b.on('close', () => {})
// → 2 warnings totalSo 448 warnings ≈ 448 distinct 2. Reproducing it without needing to reach the thresholdThe warning only fires above 10, which makes it awkward to reproduce locally where fewer listeners are attached. You don't need to reach 10 — just lower the limit so the same code path crosses it: // lower-max-listeners.js
require('node:events').defaultMaxListeners = 5NODE_OPTIONS="--trace-warnings --require ./lower-max-listeners.js" next startThis produces the identical warning (with One caveat: because of the // dump-close-stacks.js
const http = require('node:http')
const counts = new WeakMap()
const proto = http.ServerResponse.prototype
// `once()` delegates to `on()`, `prependOnceListener()` to `prependListener()`.
// Hook only the terminal two, or every registration gets counted twice.
for (const m of ['on', 'prependListener']) {
const original = proto[m]
proto[m] = function (event, listener) {
if (event === 'close') {
const n = (counts.get(this) || 0) + 1
counts.set(this, n)
console.log(`#${n} ${this.req?.url}\n${new Error().stack}`)
}
return original.call(this, event, listener)
}
}3. Measured counts: 15.5.21 vs 16.3.0Same application, same Node (20.18.0), both served with
4. Why the number is exactly 11Node warns when the count exceeds 10, i.e. on the 11th listener. In our deployed environment something outside Next adds ~5 more on top of Next's own: Next 15 was sitting exactly at the limit, and 16's +1 pushed it over. That explains the abrupt
5. All six registrations in 16.3.0Full stacks for a single App Router page request.
Two observations:
6. SuggestionEach of the six listeners looks individually legitimate. The problem is that framework internals consume 6 of Node's default budget of 10, leaving very little headroom for anything else in the process (APM/tracing, proxies, custom instrumentation). Two directions:
Environment
|
|
Follow-up to my comment above: I've now accounted for all 11 listeners and reproduced the warning locally, without needing to lower the limit. The key finding: the warning only fires on requests that go through a Full breakdown of the 11
The three added by the rewrite proxy
Two things this rules out
If you are hitting this, check
If both, you are likely at 11 on exactly those routes. On 15.5.21 the same setup lands on 10 and stays silent. Restating the suggestionNone of the 11 registrations is wrong on its own. The issue is the budget: Next's own internals take 6 of Node's default 10, and Next's own rewrite proxy takes 3 more on proxied routes — so 9 of the 10 default slots are consumed by Next itself before any user code or instrumentation is added. Calling |
|
Uh oh!
There was an error while loading. Please reload this page.
I managed to pin the issue on all the deps i'm using and it leads to Next 16.3.0.
Any feedback?
All reactions