OpenTelemetry JS SpanProcessor shutdown timeout hangs the test process in CI
A BatchSpanProcessor holds a live exporter connection and a scheduled flush timer. If the SDK is never shut down (or shutdown blocks trying to reach a dead collector), the Node event loop stays alive and the test runner hangs or times out.
What this error means
Jest reports "A worker process has failed to exit gracefully" or "did not exit one second after the test run completed", or the CI step hangs until the job timeout even though all tests passed.
Jest did not exit one second after the test run has completed.
This usually means that there are asynchronous operations that weren't stopped
in your tests. Consider running Jest with --detectOpenHandles to troubleshoot.Common causes
The SDK is never shut down after tests
BatchSpanProcessor schedules timers and keeps sockets open. Without calling sdk.shutdown() in an afterAll hook, those handles keep the process alive.
shutdown() blocks flushing to a dead collector
On shutdown the processor tries to flush queued spans to the OTLP endpoint. If nothing listens there, the flush waits out its export timeout, delaying or hanging exit.
How to fix it
Shut the SDK down in teardown
- Call
await sdk.shutdown()in a global afterAll / teardown. - Keep the export timeout short so a dead endpoint does not stall shutdown.
- For pure unit tests, disable the SDK so no processor starts.
afterAll(async () => {
await sdk.shutdown(); // flush + close exporter handles
});Use SimpleSpanProcessor + InMemory exporter in tests
An in-memory exporter has no network handle to leave open, so the process exits cleanly.
const exporter = new InMemorySpanExporter();
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));How to prevent it
- Always call sdk.shutdown() in test teardown.
- Use InMemorySpanExporter for tests to avoid open sockets.
- Set a short export/shutdown timeout so flush cannot block exit.