A Node.js library for detecting memory leaks. Track resources in your code and verify they're cleaned up properly.
leakspector helps you catch memory leaks in your code by tracking resource usage and comparing it against the initial state. While commonly used within test runners to detect leaks in code under test, it can also be used outside of tests. Currently tracks:
- Event listeners on
EventEmitterinstances - Timers
setTimeoutandsetInterval
npm install --save-dev leakspector
# or
pnpm add --save-dev leakspector
# or
yarn add --dev leakspector
# or
bun add --dev leakspectorLeakspector is best used in conjunction with a test runner like Vitest or Jest.
import { beforeEach, afterEach } from 'vitest';
import { track, check } from 'leakspector';
beforeEach(() => {
track();
});
afterEach(async () => {
await check();
});import { describe, it, expect } from 'vitest';
import { EventEmitter } from 'events';
describe('my feature', () => {
it('should clean up event listeners', () => {
const emitter = new EventEmitter();
const handler = () => {};
emitter.on('data', handler);
emitter.off('data', handler); // Properly cleaned up
// Test passes - no leaks detected
});
it('should fail if listeners leak', () => {
const emitter = new EventEmitter();
const handler = () => {};
emitter.on('data', handler);
// Forgot to remove handler - leak detected, test fails in afterEach
});
it('should clean up timers', () => {
const id = setTimeout(() => {}, 1000);
clearTimeout(id); // Properly cleaned up
// Test passes - no leaks detected
});
it('should fail if timers leak', () => {
setTimeout(() => {}, 1000);
// Forgot to clear timer - leak detected, test fails in afterEach
});
});Take snapshots of current resource state:
import { track, snapshot } from 'leakspector';
track();
// ... create some resources ...
const snap = snapshot();
// snap = {
// eventListeners: { 'EventEmitter#1': { data: 1 } },
// timers: { setTimeout: 2, setInterval: 0 }
// }For more accurate leak detection, force garbage collection before checking:
afterEach(async () => {
await check({ forceGC: true });
});Note: To use forceGC, run Node.js with the --expose-gc flag.
node --expose-gc your-script.js
# or
NODE_OPTIONS=--expose-gc your-script.jsIf using Vitest, add this to your config:
// vitest.config.ts
export default {
// ...
test: {
// ...
execArgv: ['--expose-gc'],
},
};If using Jest, configure your test script in package.json:
{
"scripts": {
"test": "NODE_OPTIONS='--expose-gc' jest"
}
}To check for leaks without failing tests:
afterEach(async () => {
await check({ throwOnLeaks: false });
// Leaks will be logged to console.error instead
});Leakspector tracks the following resources:
Tracks all EventEmitter instances and their listeners. Detects leaks when listeners are added but not removed.
track();
const emitter = new EventEmitter();
emitter.on('event', handler);
// If handler isn't removed and EventEmitter is not garbage collected before
// check() is called, a leak is detectedThe library patches EventEmitter methods (on, addListener, once,
removeListener, off) to monitor listener registration. Original methods are
restored after check() is called.
leakspector automatically identifies common EventEmitter types and provides meaningful names in error messages and snapshots:
- net.Socket:
Socket (127.0.0.1:3000)orSocket (not connected) - net.Server:
Server (127.0.0.1:3000)orServer (not listening) - fs.ReadStream:
ReadStream (/path/to/file) - fs.WriteStream:
WriteStream (/path/to/file) - child_process.ChildProcess:
ChildProcess (pid 12345) - cluster.Worker:
Worker (id 1) - http.IncomingMessage:
IncomingMessage (GET /api/users) - http.ServerResponse:
ServerResponse (200 OK) - http.ClientRequest:
ClientRequest (POST example.com /api/data)
For unknown types, fallback IDs like EventEmitter#1, EventEmitter#2 are
used.
You can register custom stringifiers to identify your own EventEmitter subclasses or third-party library types. Custom stringifiers are checked before built-in ones, allowing you to override default behavior.
import { registerEmitterStringifier } from 'leakspector';
class MyCustomEmitter extends EventEmitter {
constructor(public id: string) {
super();
}
}
registerEmitterStringifier((emitter) => {
if (emitter instanceof MyCustomEmitter) {
return `MyCustomEmitter (id: ${emitter.id})`;
}
});Register stringifiers in a setup file (e.g. vitest.setup.ts):
// vitest.setup.ts
registerEmitterStringifier((emitter) => {
if (emitter instanceof MyCustomEmitter) {
return `MyCustomEmitter (id: ${emitter.id})`;
}
});
// vitest.config.ts
export default {
// ... other config ...
setupFiles: ['vitest.setup.ts'],
};You can register multiple stringifiers. They're checked in registration order, and the first one to return a non-null/undefined string wins:
registerEmitterStringifier((emitter) => {
if (emitter instanceof TypeA) {
return `TypeA (${emitter.name})`;
}
});
registerEmitterStringifier((emitter) => {
if (emitter instanceof TypeB) {
return `TypeB (${emitter.id})`;
}
});Return null, undefined, or omit the return statement to pass through to the
next stringifier:
registerEmitterStringifier((emitter) => {
if (emitter instanceof MyType) {
return `MyType (${emitter.id})`;
}
});Tracks setTimeout and setInterval calls. Detects leaks when timers are
created but not cleared.
track();
const id = setTimeout(() => {}, 1000);
// If timer isn't cleared before check() is called, a leak is detected
clearTimeout(id); // Properly cleaned upThe library patches global setTimeout, setInterval, clearTimeout, and
clearInterval functions to monitor timer creation and cleanup. Original
functions are restored after check() is called.
Starts tracking resources in your code. When used in tests, call this in
beforeEach before executing code that creates resources you want to track.
Parameters:
options.trackers(optional): Which trackers to enable. Defaults to"all"if not provided."all": Enable all available trackers (event listeners and timers)TrackerName[]: Array of specific tracker names to enable (e.g.,["eventListeners"],["timers"], or["eventListeners", "timers"])
Throws: Error if tracking is already active. Call check() first to
reset.
Examples:
// Enable all trackers (default)
track();
// Explicitly enable all trackers
track({ trackers: 'all' });
// Enable only event listeners
track({ trackers: ['eventListeners'] });
// Enable only timers
track({ trackers: ['timers'] });
// Enable multiple specific trackers
track({ trackers: ['eventListeners', 'timers'] });Checks for leaks by comparing current resource usage against the initial state.
When used in tests, call this in afterEach to verify resources were cleaned
up.
Parameters:
options.forceGC(optional): Whether to force garbage collection before checking. Defaults tofalse.options.throwOnLeaks(optional): Whether to throw an error if leaks are detected. Defaults totrue.options.format(optional): Output format for error messages. Defaults to"summary"."short": Terse, leak count only"summary": List of leaks with counts (default behavior)"details": Detailed output with stack traces showing where leaks were created
Returns: Promise<void>
Throws:
Errorif tracking is not active (calltrack()first).Errorif leaks are detected andthrowOnLeaksistrue. Errors from multiple trackers are aggregated.
Note: After calling check(), tracking is reset. You must call track()
again to start a new tracking session. When used in tests, call track() again
in the next beforeEach. The function checks all active trackers and aggregates
any errors found.
await check({ format: 'short' });
// Error: Event listener leaks detected: 5 leaked listener(s)
//
// Timer leaks detected: 2 leaked timer(s)await check({ format: 'summary' });
// Error: Event listener leaks detected:
// Event 'EventEmitter#1.error': expected 0 listener(s), found 1 (+1 leaked)
// Event 'EventEmitter#1.data': expected 0 listener(s), found 1 (+1 leaked)
//
// Timer leaks detected:
// setTimeout path/to/file.ts:42:5
// setInterval path/to/file.ts:88:12await check({ format: 'details' });
// Error: Event listener leaks detected:
// EventEmitter#1
// > 'error': expected 0 listener(s), found 2 (+2 leaked)
// * on('error') path/to/event-listening-file.ts:301:4
// * once('error') path/to/other/file.ts:22:2
//
// Timer leaks detected:
// setTimeout path/to/file.ts:42:5
// setInterval path/to/file.ts:88:12Creates a snapshot of all currently active trackers' state. Returns a record
mapping tracker names to their snapshots. Only includes trackers that are
currently active (i.e., have been started via track()).
Returns: Snapshot - A record of active tracker names to their snapshots.
The return type structure:
type Snapshot = {
eventListeners?: ListenersSnapshot;
timers?: TimersSnapshot;
};eventListeners: A record mapping emitter identifiers to their event listener countstimers: A record mapping timer types to their counts
Example:
track();
const emitter = new EventEmitter();
emitter.on('data', handler);
setTimeout(() => {}, 1000);
const snap = snapshot();
// snap = {
// eventListeners: { 'EventEmitter#1': { data: 1 } },
// timers: { setTimeout: 1, setInterval: 0 }
// }Convenience object providing access to event listener leak detection functions.
Properties:
track()- Starts tracking event listeners on all EventEmitter instances.snapshot()- Creates a snapshot of current listeners. Returns aListenersSnapshotmapping emitter identifiers to their event listener counts.check(options?)- Checks for leaks and restores original EventEmitter prototype methods.
Example:
import { eventListeners } from 'leakspector';
eventListeners.track();
const emitter = new EventEmitter();
emitter.on('data', handler);
const snap = eventListeners.snapshot();
// snap = { 'EventEmitter#1': { data: 1 } }
await eventListeners.check();Convenience object providing access to timer leak detection functions.
Properties:
track()- Starts trackingsetTimeoutandsetIntervalcalls.snapshot()- Creates a snapshot of current timers. Returns aTimersSnapshotmapping timer types to their counts.check(options?)- Checks for leaks and restores original timer functions.
Example:
import { timers } from 'leakspector';
timers.track();
setTimeout(() => {}, 1000);
const snap = timers.snapshot();
// snap = { setTimeout: 1, setInterval: 0 }
await timers.check();Copyright 2025 Charles Francoise