npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

why-is-node-blocked

v0.1.0

Published

Shows the JavaScript stack that is blocking the Node.js event loop

Readme

why-is-node-blocked

Find the line of JavaScript that is blocking your Node.js event loop.

$ npx why-is-node-blocked example.mjs
why-is-node-blocked: event loop blocked for over 1000ms
    at resizeImage (example.mjs:7:3)
    at <anonymous> (example.mjs:19:1)
why-is-node-blocked: event loop resumed after 2006ms
why-is-node-blocked: event loop blocked for over 1000ms
    at isValidEmail (example.mjs:11:21)
    at <anonymous> (example.mjs:21:1)
why-is-node-blocked: event loop resumed after 4140ms
why-is-node-blocked: event loop blocked for over 1000ms
    at getStderr (node:internal/bootstrap/switches/is_main_thread:174:3)
    at execSync (node:child_process:986:13)
    at gitRevision (example.mjs:15:10)
    at <anonymous> (example.mjs:23:1)
why-is-node-blocked: event loop resumed after 2119ms

Busy loops, catastrophic regular expressions, synchronous fs and child_process calls: when the main thread stops turning the event loop, you get the stack it is stuck in. No code changes, no dependencies, nothing running on the main thread while the loop is healthy.

Usage

Run a script with it:

npx why-is-node-blocked server.js

Or install it and load it before your app:

npm install why-is-node-blocked
node --import why-is-node-blocked/register server.js
NODE_OPTIONS="--import why-is-node-blocked/register" npm start

The threshold defaults to 1000ms; set WHY_IS_NODE_BLOCKED_MS to change it. Your app starts once the watcher thread is running, which adds a few tens of milliseconds to startup (about 45ms on Linux x64). The wait is best effort: the app goes ahead without a watcher that fails to start, and stops waiting for a slow one when a 2-second timer fires.

To send reports to your logger instead of stderr:

import { watch } from 'why-is-node-blocked';

await watch({
  thresholdMs: 500,
  onBlock({ durationMs, stack, reason }) {
    logger.warn({ durationMs, stack, reason }, 'event loop blocked');
  },
});

watch() resolves once the watcher thread is running, when it fails or exits before that, or when a 2-second timer fires; it never rejects, and errors are emitted as process warnings. A stall that ends before the watcher runs goes unseen. Calling it again returns the same promise. onBlock runs on the main thread once the loop turns again. stack is an array of { functionName, url, line, column }, or null with a reason when no stack could be taken. The stderr reporter writes from another thread as soon as the stall is detected, so it also reports a loop that never recovers.

Requires Node.js 22.15 or later.

How it works

Every 50ms the main thread writes the time (process.hrtime.bigint(), a monotonic clock) into a SharedArrayBuffer. A worker thread reads it, and when it falls behind by more than the threshold, the worker opens an inspector session to the main thread (Session.connectToMainThread()), pauses it for an instant with Debugger.pause, reads the call frames and resumes it.

The pause is delivered through a V8 interrupt, which the main thread handles at its next JavaScript stack check. A thread stuck in a synchronous system call is caught as soon as the call returns, still inside it, which is why the execSync above shows up with its caller.

Why not the CPU profiler

Starting V8's sampling profiler is cheaper than enabling the debugger, but samples taken right after it starts lose the frame of a function running in on-stack-replaced (OSR) code. A hot loop is exactly that, so the profiler blames the loop's caller.

Cost

While the loop is healthy: one timer and one atomic store every 50ms on the main thread, with no measurable change in throughput in our benchmark.

When a stall is caught, the capture holds the main thread a little longer: Debugger.enable reports every loaded script, and the first time it also hashes their source (about 6ms per MB). Measured on Linux x64, a capture took 20 to 40ms in a small app (about 100 scripts) and 0.5 to 1 second with 30,000 scripts loaded. Part of that cost comes from V8 capturing a stack trace for every replayed script; a fix is in review.

Captures only happen when the loop has already been blocked for longer than the threshold.

Caveats

  • Only the main thread is watched.
  • A synchronous call that never returns is reported as blocked, without a stack.
  • Native code that does not check for V8 interrupts, like JSON.parse on a huge string, is caught only after it returns, so the stack shows the statement that follows it.
  • If DevTools or another inspector client has the thread paused at a breakpoint, the report says so and leaves the pause alone.
  • Under the permission model it needs --allow-worker, or it warns and stays off. Stacks also need --allow-inspector on Node.js versions that have it; without it, stalls are reported without a stack. Reading WHY_IS_NODE_BLOCKED_MS needs --allow-env.
  • A process that exits right after a stall does not get the "resumed" line.
  • Before Node.js 26.1, if the system refuses to start the script (for example an environment over the kernel's size limits), the why-is-node-blocked command aborts instead of printing the error. --import why-is-node-blocked/register is not affected.

Compared to

  • blocked-at uses async_hooks to point at where the blocking callback was scheduled, not where it is stuck, and adds overhead to every async operation.
  • Sentry's SDK detects blocked event loops too, as part of Sentry.
  • why-is-node-running answers the opposite question: what keeps the process from exiting.

License

MIT