Skip to content
Latchkey

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).

node
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 transport

Common 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

  1. Call logger.flush() then close the transport in afterAll.
  2. Await the transport`s exit so the worker thread ends.
  3. For unit tests, log to a plain sync destination with no worker.
test/setup.js
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.

test/setup.js
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.

Related guides

Tired of flaky CI? Latchkey auto-heals failed jobs and retries them for you. Start free →