Skip to content

Memory leak: AbortSignal listeners not cleaned up after fetch() completes #393

@misablaha

Description

@misablaha

When a long-lived AbortSignal is reused across many fetch() calls, event listeners accumulate on the signal and are never removed after the request completes. This causes linear RSS growth proportional to the number of completed requests.

Root cause

Two listeners are registered on the signal per request and neither is cleaned up:

1. fetch()waitForAbort promise (index.wrapper.js)

const waitForAbort = new Promise((_, reject) => {
    signal?.addEventListener?.("abort", () => {
        reject(signal.reason);
    }, { once: true });
});

If the signal is never aborted, this promise never settles and the listener persists on the signal indefinitely, preventing GC of the closure and everything it captures.

2. #wrapResponse() — response abort listener

signal?.addEventListener?.("abort", () => {
    originalResponse.abort();
});

This listener holds a reference to originalResponse (native Rust object) and is registered without { once: true }. It is never removed after the response is consumed.

Reproduction

import { Impit } from "impit";

const controller = new AbortController();
const impit = new Impit({ browser: "chrome" });

for (let i = 0; i < 5000; i++) {
  const response = await impit.fetch("https://example.com", {
    signal: controller.signal,
  });
  await response.text();

  if (i % 100 === 0) {
    console.log(`${i} requests, RSS: ${Math.round(process.memoryUsage().rss / 1024 / 1024)} MB`);
  }
}

Expected: RSS stabilizes after warmup.
Actual: RSS grows linearly with each request.

Impact

In a worker processing queue tasks at high concurrency (100 concurrent requests sharing one AbortController), RSS grows ~90 MB/min and the process OOM-kills within minutes on memory-constrained environments (e.g. 1Gi Cloud Run).

Suggested fix

Remove listeners after the request completes, e.g. via AbortController per request internally or explicit removeEventListener in a finally block.

Metadata

Metadata

Assignees

Labels

t-toolingIssues with this label are in the ownership of the tooling team.

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions