An SSE endpoint pushing updates to clients. Subscribes to an event bus. Does not unsubscribe when the client disconnects. Listeners accumulate; memory grows.
The vulnerable diff
// api/sse-updates.ts
sseRouter.get("/updates", (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.flushHeaders();
// BUG: subscribe stays attached after client disconnects
bus.on("update", (msg) => {
res.write(`data: ${JSON.stringify(msg)}\n\n`);
});
});What is wrong
Server-Sent Events are long-lived HTTP connections that push updates. The handler typically subscribes to an internal event bus and writes each event to the response. Without cleanup, when the client disconnects, the subscription remains — the bus still calls the handler, the handler tries to write to a dead response, errors accumulate, the closure pins the response in memory. Over time, listeners stack up.
The attack
Reproducer: 100 SSE connections opened, all clients close. Server still has 100 listeners.
# After 100 cycles: bus.listenerCount("update") = 100.
# Each listener holds the response object + closure.
# Memory: ~50 MB just from leaked SSE handlers.OOM eventually.
Mesrai's review comment
mesraipilot · Bot · reviewed 1 min ago
[mesrai] [code-review] [Performance] [Listener-Leak] [high]
SSE subscription not removed on client disconnect.
Listen for the request close event and unsubscribe:
sseRouter.get("/updates", (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.flushHeaders();
const onUpdate = (msg: Update) => {
res.write(`data: ${JSON.stringify(msg)}\n\n`);
};
bus.on("update", onUpdate);
req.on("close", () => {
bus.off("update", onUpdate);
});
});
Also set up a heartbeat so dead connections detect themselves:
const ping = setInterval(() => res.write(": ping\n\n"), 15_000);
req.on("close", () => clearInterval(ping));The fix
// api/sse-updates.ts — fixed
sseRouter.get("/updates", (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.flushHeaders();
const onUpdate = (msg: Update) => {
res.write(`data: ${JSON.stringify(msg)}\n\n`);
};
bus.on("update", onUpdate);
const ping = setInterval(() => res.write(": ping\n\n"), 15_000);
req.on("close", () => {
bus.off("update", onUpdate);
clearInterval(ping);
});
});Subscribe in handler, unsubscribe on close. Heartbeat keeps the connection alive through proxies. All cleanup attached to req.close.
Why human review missed it
SSE / WebSocket listener leaks are common in long-lived-connection code. Mesrai catches subscribe-shape calls inside handlers without paired close-event cleanup.
Related rules + further reading
Mesrai rule pack: performance/long-lived-connection-cleanup — flags SSE/WS subscriptions without close cleanup.
MDN: Server-Sent Events.
Node EventEmitter: listenerCount.
Takeaway
Subscribe + unsubscribe on close. Heartbeat for proxy keepalive. Mesrai catches every SSE without cleanup.