nextjs - ✅(Solved) Fix Process Listener Leak in Serverless/Firebase Cloud Functions Environment [1 pull requests, 2 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#86598Fetched 2026-04-08 02:10:08
View on GitHub
Comments
2
Participants
3
Timeline
6
Reactions
0
Timeline (top)
commented ×2cross-referenced ×1issue_type_added ×1referenced ×1

Error Message

export const getServerSideProps: GetServerSideProps = async () => { const uncaughtListeners = process.listeners('uncaughtException'); const unhandledListeners = process.listeners('unhandledRejection');

console.log(uncaughtException listeners: ${uncaughtListeners.length}); console.log(Listener names: ${uncaughtListeners.map(l => l.name || 'anonymous').join(', ')});

console.log(unhandledRejection listeners: ${unhandledListeners.length});

return { props: {} }; };

Root Cause

The issue is in node_modules/next/dist/server/lib/router-server.js (lines ~566-567 in the compiled output):

process.on('uncaughtException', logError.bind(null, 'uncaughtException'));
process.on('unhandledRejection', logError.bind(null, 'unhandledRejection'));

This code runs during the initialize() function. In serverless environments, initialize() appears to be called on each request (or periodically), adding new listeners without removing previous ones.

Fix Action

Workaround

We implemented a cleanup function that removes excess bound logError listeners before each request:

const cleanupNextJsListenerLeak = () => {
  if (typeof process === 'undefined') return;

  const targetListenerName = 'bound logError';
  const maxAllowed = 1;

  for (const eventName of ['uncaughtException', 'unhandledRejection']) {
    const listeners = (process as NodeJS.EventEmitter).listeners(eventName);
    const targetListeners = listeners.filter(
      (l) => (l as { name?: string }).name === targetListenerName
    );

    if (targetListeners.length > maxAllowed) {
      const toRemove = targetListeners.slice(0, targetListeners.length - maxAllowed);
      for (const listener of toRemove) {
        (process as NodeJS.EventEmitter).removeListener(
          eventName,
          listener as (...args: unknown[]) => void
        );
      }
    }
  }
};

This keeps listener counts stable after 2,670+ requests over 10+ hours.

PR fix notes

PR #2: Refactor tests and router-server for listener cleanup and code clarity

Description (problem / solution / changelog)

What?

Refactored tests and router-server to ensure proper listener cleanup and removed redundant TypeScript error comments.

Why?

Improves resource management and overall code clarity by cleaning up redundant logic.

How?

  • Updated the router-server logic for listeners cleanup.
  • Removed outdated TypeScript error comments in affected files.
  • Enhanced test cases for improved resource lifecycle validation.

Fixes #86598

-->

Changed files

  • packages/next/src/server/lib/router-server.ts (modified, +3/-7)
  • test/unit/router-server-process-listeners.test.ts (modified, +73/-70)

Code Example

export const getServerSideProps: GetServerSideProps = async () => {
  const uncaughtListeners = process.listeners('uncaughtException');
  const unhandledListeners = process.listeners('unhandledRejection');
  
  console.log(`uncaughtException listeners: ${uncaughtListeners.length}`);
  console.log(`Listener names: ${uncaughtListeners.map(l => l.name || 'anonymous').join(', ')}`);
  
  console.log(`unhandledRejection listeners: ${unhandledListeners.length}`);
  
  return { props: {} };
};

---

uncaughtException listeners: 4
Listener names: domainUncaughtExceptionClear, anonymous, anonymous, bound logError
unhandledRejection listeners: 3

---

uncaughtException listeners: 62
Listener names: domainUncaughtExceptionClear, anonymous, anonymous, bound logError, bound logError, bound logError, ... (59 more "bound logError")
unhandledRejection listeners: 61

---

MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 
11 uncaughtException listeners added to [process]

---

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.0.0: Wed Sep 17 21:38:03 PDT 2025; root:xnu-12377.1.9~141/RELEASE_ARM64_T8112
  Available memory (MB): 16384
  Available CPU cores: 8
Binaries:
  Node: 20.19.4
  npm: 11.5.2
  Yarn: N/A
  pnpm: N/A
Relevant Packages:
  next: 16.0.5 // Latest available version is detected (16.0.5).
  eslint-config-next: N/A
  react: 19.2.0
  react-dom: 19.2.0
  typescript: 5.9.3
Next.js Config:
  output: N/A

---

process.on('uncaughtException', logError.bind(null, 'uncaughtException'));
process.on('unhandledRejection', logError.bind(null, 'unhandledRejection'));

---

const cleanupNextJsListenerLeak = () => {
  if (typeof process === 'undefined') return;

  const targetListenerName = 'bound logError';
  const maxAllowed = 1;

  for (const eventName of ['uncaughtException', 'unhandledRejection']) {
    const listeners = (process as NodeJS.EventEmitter).listeners(eventName);
    const targetListeners = listeners.filter(
      (l) => (l as { name?: string }).name === targetListenerName
    );

    if (targetListeners.length > maxAllowed) {
      const toRemove = targetListeners.slice(0, targetListeners.length - maxAllowed);
      for (const listener of toRemove) {
        (process as NodeJS.EventEmitter).removeListener(
          eventName,
          listener as (...args: unknown[]) => void
        );
      }
    }
  }
};
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

https://github.com/erickborquez/nextjs-process-listener-leak

To Reproduce

  1. Deploy a Next.js app to Firebase Cloud Functions (or similar serverless environment where the process persists across requests)
  2. Create a page with getServerSideProps that logs process listener counts:
export const getServerSideProps: GetServerSideProps = async () => {
  const uncaughtListeners = process.listeners('uncaughtException');
  const unhandledListeners = process.listeners('unhandledRejection');
  
  console.log(`uncaughtException listeners: ${uncaughtListeners.length}`);
  console.log(`Listener names: ${uncaughtListeners.map(l => l.name || 'anonymous').join(', ')}`);
  
  console.log(`unhandledRejection listeners: ${unhandledListeners.length}`);
  
  return { props: {} };
};
  1. Make repeated requests to the page
  2. Observe listener counts growing with each request

Live demo App: https://nextjs-process-listener-leak.web.app/

Current vs. Expected behavior

Expected Behavior

Process listener counts should remain stable across requests. Listeners should either:

  • Be registered once per process lifecycle, OR
  • Be cleaned up before re-registering

Actual Behavior

Request #1 (Baseline)

uncaughtException listeners: 4
Listener names: domainUncaughtExceptionClear, anonymous, anonymous, bound logError
unhandledRejection listeners: 3

Request #50

uncaughtException listeners: 62
Listener names: domainUncaughtExceptionClear, anonymous, anonymous, bound logError, bound logError, bound logError, ... (59 more "bound logError")
unhandledRejection listeners: 61

The bound logError listeners accumulate at approximately 1 per request.

After ~10 requests, Node.js emits:

MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 
11 uncaughtException listeners added to [process]

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.0.0: Wed Sep 17 21:38:03 PDT 2025; root:xnu-12377.1.9~141/RELEASE_ARM64_T8112
  Available memory (MB): 16384
  Available CPU cores: 8
Binaries:
  Node: 20.19.4
  npm: 11.5.2
  Yarn: N/A
  pnpm: N/A
Relevant Packages:
  next: 16.0.5 // Latest available version is detected (16.0.5).
  eslint-config-next: N/A
  react: 19.2.0
  react-dom: 19.2.0
  typescript: 5.9.3
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

Not sure

Which stage(s) are affected? (Select all that apply)

Other (Deployed)

Additional context

I'm hosting the NextJs Application through Firebase Web Frameworks. It's not reproducible locally, it has to be deployed in order for the leak to appear.

After doing some research we found the root cause and a workaround

Root Cause

The issue is in node_modules/next/dist/server/lib/router-server.js (lines ~566-567 in the compiled output):

process.on('uncaughtException', logError.bind(null, 'uncaughtException'));
process.on('unhandledRejection', logError.bind(null, 'unhandledRejection'));

This code runs during the initialize() function. In serverless environments, initialize() appears to be called on each request (or periodically), adding new listeners without removing previous ones.

Impact

  1. Memory leak - Each listener retains references, preventing garbage collection
  2. MaxListenersExceededWarning - Noisy warnings in logs after ~10 requests
  3. Potential performance degradation - Many duplicate handlers firing on errors
  4. Instance recycling - In Firebase Cloud Functions, excessive memory growth can cause the instance to be recycled

Workaround

We implemented a cleanup function that removes excess bound logError listeners before each request:

const cleanupNextJsListenerLeak = () => {
  if (typeof process === 'undefined') return;

  const targetListenerName = 'bound logError';
  const maxAllowed = 1;

  for (const eventName of ['uncaughtException', 'unhandledRejection']) {
    const listeners = (process as NodeJS.EventEmitter).listeners(eventName);
    const targetListeners = listeners.filter(
      (l) => (l as { name?: string }).name === targetListenerName
    );

    if (targetListeners.length > maxAllowed) {
      const toRemove = targetListeners.slice(0, targetListeners.length - maxAllowed);
      for (const listener of toRemove) {
        (process as NodeJS.EventEmitter).removeListener(
          eventName,
          listener as (...args: unknown[]) => void
        );
      }
    }
  }
};

This keeps listener counts stable after 2,670+ requests over 10+ hours.

extent analysis

TL;DR

To fix the process listener leak in a Next.js app deployed to a serverless environment like Firebase Cloud Functions, implement a cleanup function to remove excess listeners before each request.

Guidance

  • Identify the source of the listener leak, which in this case is the repeated addition of bound logError listeners in node_modules/next/dist/server/lib/router-server.js.
  • Implement the provided cleanupNextJsListenerLeak function to remove excess listeners before each request, ensuring that the listener count remains stable.
  • Verify the effectiveness of the workaround by monitoring listener counts and checking for the absence of MaxListenersExceededWarning warnings.
  • Consider the potential impact of the leak on memory usage and performance, and take steps to mitigate these effects, such as implementing instance recycling in Firebase Cloud Functions.

Example

The provided cleanupNextJsListenerLeak function can be used as an example of how to remove excess listeners:

const cleanupNextJsListenerLeak = () => {
  // ...
};

This function can be called before each request to ensure that the listener count remains stable.

Notes

The provided workaround is specific to the identified root cause and may not be applicable to other scenarios. Additionally, the effectiveness of the workaround may depend on the specific deployment environment and configuration.

Recommendation

Apply the provided workaround by implementing the cleanupNextJsListenerLeak function to remove excess listeners before each request, as it has been shown to keep listener counts stable after an extended period of requests.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING