Skip to content

Memory leak in long-running process — JS heap exhaustion after ~6 days #55

Description

@gthieleb

Bug: Memory leak in long-running email-mcp process

Summary

email-mcp's JavaScript heap grows steadily over days of continuous operation until it exhausts available memory and crashes with OOM. This affects any long-running deployment (HTTP server, Docker container, systemd service), not a specific transport mode.

Environment

  • Transport: HTTP (Streamable HTTP), but same runServer() code path as stdio
  • Node.js: 24 (node:24-slim)
  • Accounts: 2 IMAP accounts
  • Container memory limit: 512 Mi
  • Watcher: disabled ([settings.watcher] enabled = false)

Symptoms

  • Process starts at ~187 Mi
  • Heap grows steadily to ~254 MB over 6 days (~5.7 days uptime, 494741809 ms)
  • GC cannot keep up: Mark-Compact (reduce) 253.7 → 253.5 MB — almost nothing freed
  • Crash: FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
  • Exit code 139 (SIGSEGV after OOM handler)

Logs (anonymized — no account data)

email-mcp HTTP server listening on :8080
  Endpoint : http://0.0.0.0:8080/mcp
  Health   : http://0.0.0.0:8080/health

<--- Last few GCs --->

[1:0x88ca000] 494741295 ms: Mark-Compact (reduce) 253.7 (257.0) -> 253.5 (256.8) MB, pooled: 0 MB, 482.95 / 0.00 ms  (average mu = 0.999, current mu = ...)
[1:0x88ca000] 494741809 ms: Mark-Compact 254.6 (256.8) -> 254.0 (258.2) MB, pooled: 0 MB, 511.13 / 0.00 ms allocation failure; scavenge might not succeed

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----

 1: node::OOMErrorHandler(...)
 2-14: [internal node frames]

Analysis

The watcher is disabled, so the leak is not from IMAP IDLE. Likely sources:

  1. SchedulersetInterval(checkAndSend, 60_000) runs ~8,640 times over 6 days. If IMAP connections/results aren't fully released after each check, they accumulate.

  2. ConnectionManager — persistent ImapFlow clients (Map<string, ImapFlow>) may accumulate internal buffers (mailparser, fetch results) that aren't GC'd properly over time.

  3. ImapService cachesmailboxListCache, labelStrategies, labelStrategyPending Maps grow and stale entries may not be evicted. The code itself notes: "buffer as a JS string — that would defeat the parse cap and pin memory" (imap.service.ts:451).

  4. HTTP sessionsStreamableHTTPServerTransport session state may not be fully cleaned up after client disconnects.

Why this isn't seen in typical stdio usage

In stdio mode, email-mcp is typically spawned per-session (Claude Desktop, Cursor) and lives for minutes to hours — not enough time for the slow leak to manifest. Long-running deployments (HTTP server, Docker, systemd) expose it.

Reproduction

  1. Start email-mcp in HTTP mode: node dist/main.js http 8080
  2. Configure 2 IMAP accounts
  3. Leave running for 5-7 days with periodic tool calls
  4. Observe heap growth via --trace-gc or process memory monitoring
  5. Process crashes with OOM (exit 139)

Workaround

  • Increase memory limit to 1 Gi
  • Daily process restart (CronJob in k8s, systemd timer, or equivalent)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions