fix: memory leak in SSE session reset (#542)

When SSE sessions are recreated every 5 minutes, the old session's MCP
server was not being closed, causing:
- SimpleCache cleanup timer continuing to run indefinitely
- Database connections remaining open
- Cached data (~50-100MB per session) persisting in memory

Added server.close() call before transport.close() in resetSessionSSE(),
mirroring the existing cleanup pattern in removeSession().

Fixes #542

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2026-01-21 13:04:04 +01:00
parent 0f15b82f1e
commit f5088918aa
3 changed files with 29 additions and 4 deletions

View File

@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [2.33.4] - 2026-01-21
### Fixed
- **Memory leak in SSE session reset** (Issue #542): Fixed memory leak when SSE sessions are recreated every 5 minutes
- Root cause: `resetSessionSSE()` only closed the transport but not the MCP server
- This left the SimpleCache cleanup timer (60-second interval) running indefinitely
- Database connections and cached data (~50-100MB per session) persisted in memory
- Fix: Added `server.close()` call before `transport.close()`, mirroring the existing cleanup pattern in `removeSession()`
- Impact: Prevents ~288 leaked server instances per day in long-running HTTP deployments
## [2.33.3] - 2026-01-21 ## [2.33.3] - 2026-01-21
### Changed ### Changed

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.33.3", "version": "2.33.4",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -677,11 +677,25 @@ export class SingleSessionHTTPServer {
private async resetSessionSSE(res: express.Response): Promise<void> { private async resetSessionSSE(res: express.Response): Promise<void> {
// Clean up old session if exists // Clean up old session if exists
if (this.session) { if (this.session) {
const sessionId = this.session.sessionId;
logger.info('Closing previous session for SSE', { sessionId });
// Close server first to free resources (database, cache timer, etc.)
// This mirrors the cleanup pattern in removeSession() (issue #542)
// Handle server close errors separately so transport close still runs
if (this.session.server && typeof this.session.server.close === 'function') {
try {
await this.session.server.close();
} catch (serverError) {
logger.warn('Error closing server for SSE session', { sessionId, error: serverError });
}
}
// Close transport last - always attempt even if server.close() failed
try { try {
logger.info('Closing previous session for SSE', { sessionId: this.session.sessionId });
await this.session.transport.close(); await this.session.transport.close();
} catch (error) { } catch (transportError) {
logger.warn('Error closing previous session:', error); logger.warn('Error closing transport for SSE session', { sessionId, error: transportError });
} }
} }