Skip to content

fix: prevent SSE listener leak in console-logs stream#751

Open
zerray wants to merge 1 commit intodecolua:masterfrom
zerray:pr/sse-listener-leak-fix
Open

fix: prevent SSE listener leak in console-logs stream#751
zerray wants to merge 1 commit intodecolua:masterfrom
zerray:pr/sse-listener-leak-fix

Conversation

@zerray
Copy link
Copy Markdown

@zerray zerray commented Apr 24, 2026

Summary

MaxListenersExceededWarning: Possible EventTarget memory leak detected. 51 line listeners added to [EventEmitter] (same for clear) appears after many client reconnects on the console-logs SSE endpoint. Node memory grows slowly alongside the listener count.

Root cause

src/app/api/translator/console-logs/stream/route.js attaches line / clear listeners on the singleton consoleEmitter inside ReadableStream.start(), and only removes them in cancel(). In Next.js 16 / Node fetch runtime, ReadableStream.cancel() is not reliably invoked when the client aborts the connection (tab close, navigation, reload), so listeners accumulate every time the dashboard reconnects. The buffered-logs emitter has setMaxListeners(50), so the warning fires on the 51st leaked connection.

request.signal (the AbortSignal on the Request) does fire on client disconnect — it is the authoritative disconnect source here.

Fix

  • Add request.signal.addEventListener("abort", cleanup, { once: true }) as the primary disconnect hook.
  • Keep cancel() as a fallback.
  • Make the cleanup idempotent via a state.closed guard so abort, cancel, and enqueue-failure paths can all call it safely.
  • Centralize emitter.off(...) + clearInterval(keepalive) inside the single cleanup() function.

No behavior change for clients; only server-side listener bookkeeping is affected. The usage/stream SSE route has the same pattern and is a candidate for the same fix in a follow-up — this PR keeps the change minimal and focused on the one route where the warning is observed.

Test plan

  • Start the app, open the dashboard, then reload the page 60+ times (or open/close the console-logs drawer rapidly)
  • listenerCount("line") on the emitter should return to ~1 after each client disconnect, not accumulate
  • No MaxListenersExceededWarning in server logs
  • Log lines still stream live to the UI; clear action still reaches connected clients
  • 25s keepalive ping still fires on idle connections; interval is cleared on disconnect

🤖 Generated with Claude Code

The ReadableStream cancel() callback is not reliably invoked on client
disconnect under Next.js, causing emitter listeners (line/clear) and the
keepalive interval to accumulate, eventually triggering
MaxListenersExceededWarning.

Use request.signal as the primary disconnect trigger, with cancel() and
enqueue failures as fallbacks. Cleanup is idempotent via state.closed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant