diff --git a/CHANGELOG.md b/CHANGELOG.md index b2eb15b..f2a1b99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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 ### Changed diff --git a/package.json b/package.json index 3ffd0a5..a6301da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp", - "version": "2.33.3", + "version": "2.33.4", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/http-server-single-session.ts b/src/http-server-single-session.ts index ef9e547..14a7df2 100644 --- a/src/http-server-single-session.ts +++ b/src/http-server-single-session.ts @@ -677,11 +677,25 @@ export class SingleSessionHTTPServer { private async resetSessionSSE(res: express.Response): Promise { // Clean up old session if exists 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 { - logger.info('Closing previous session for SSE', { sessionId: this.session.sessionId }); await this.session.transport.close(); - } catch (error) { - logger.warn('Error closing previous session:', error); + } catch (transportError) { + logger.warn('Error closing transport for SSE session', { sessionId, error: transportError }); } }