Skip to content

Memory leak in fetch() with TLS client certificates (mTLS) #27358

@slucascosta

Description

@slucascosta

What version of Bun is running?

1.3.9

What platform is your computer?

Linux x86_64 (Docker: oven/bun:1.3.9)

What steps can reproduce the bug?

git clone https://github.com/slucascosta/bun-mtls-memory-leak.git
cd bun-mtls-memory-leak
docker compose up --build

3 tests run against a local HTTPS server:

  1. test-http -- HTTPS fetch without client cert (control)
  2. test-mtls -- HTTPS fetch with client cert via tls option (leaks)
  3. test-node-mtls -- Same as 2 but in Node.js 24 (control)

Each test: 3 rounds x 10,000 requests @ 100 req/s, with Bun.gc(true) + 5s pause between rounds.

Minimal reproduction

import fs from "fs";

const cert = fs.readFileSync("data/certs/server.crt", "utf-8");
const key = fs.readFileSync("data/certs/server.key", "utf-8");

for (let round = 1; round <= 3; round++) {
  for (let i = 0; i < 10_000; i++) {
    try {
      const res = await fetch("https://localhost:8443/", {
        tls: { cert, key, rejectUnauthorized: false },
      });
      await res.text();
    } catch {}
  }
  Bun.gc(true);
  await Bun.sleep(5000);
  const m = process.memoryUsage();
  console.log(`Round ${round}: RSS=${Math.round(m.rss / 1024 / 1024)}MB`);
}
// RSS grows ~35MB per round and is never reclaimed.

What is the expected behavior?

RSS should remain stable across rounds. Certificates are loaded once, response bodies are fully consumed, and GC is explicitly called between rounds.

What do you see instead?

RSS grows linearly with each fetch() that uses the tls option and is never reclaimed.

Test Runtime Requests RSS growth Leak?
HTTPS (no client cert) Bun 1.3.9 30,000 +20MB No
HTTPS + client cert (tls) Bun 1.3.9 30,000 +109MB Yes
HTTPS + client cert Node.js 24.13.1 30,000 +22MB No
  • The only difference between test-http and test-mtls is { tls: { cert, key } } in fetch options
  • Heap stays at 1MB -- the leak is in native memory, not JS heap
  • The leak is linear: ~3.6KB/request (~36MB per 10k requests)
  • Node.js with the same workload does not leak

Suspected root cause

Each fetch() call with a tls option appears to allocate a new native SSL context that is never freed. Consistent with unreleased BoringSSL SSL_CTX or SSL objects.

Related issues

Related fixes (may not cover this scenario)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions