pino logger leaves an open handle ("Jest did not exit") in CI
pino transports (pino.transport, pino-pretty, pino/file) run in a worker thread with its own message port. If the logger is not flushed and its transport not closed after tests, Jest reports a leaked handle and the process hangs until timeout.
What this error means
Jest warns "Jest did not exit one second after the test run has completed" and --detectOpenHandles shows a MESSAGEPORT / worker-thread handle owned by a pino transport (thread-stream).
Jest has detected the following 1 open handle potentially keeping Jest
from exiting:
MESSAGEPORT
at new ThreadStream (node_modules/thread-stream/index.js:...)
at Pino transportCommon causes
The pino transport worker is never closed
A transport target (pino/file, pino-pretty) opens a thread-stream worker. Without flushing/closing the logger in teardown, that worker`s message port keeps the event loop alive.
Logger created per test without cleanup
Tests build a pino instance with a transport but never dispose it, leaking one worker handle per instance.
How to fix it
Flush and close the logger in teardown
- Call
logger.flush()then close the transport in afterAll. - Await the transport`s exit so the worker thread ends.
- For unit tests, log to a plain sync destination with no worker.
afterAll(async () => {
await new Promise((res) => logger.flush(res));
logger[Symbol.for('pino.transport')]?.end?.();
});Use a synchronous destination in tests
A sync destination (no transport worker) leaves no open handle to keep Jest alive.
const pino = require('pino');
const logger = pino(pino.destination({ sync: true }));How to prevent it
- Flush and close pino transports in test teardown.
- Use a synchronous destination for unit tests.
- Run Jest with --detectOpenHandles to catch leaked logger workers.