Node.js runs your application code on a single thread. This is a feature — until the event loop gets blocked, at which point everything queues behind it and that one core goes to 100%.
Why one core is always the ceiling. The V8 event loop is fundamentally single-threaded. Worker threads and libuv's thread pool handle IO asynchronously, but your JavaScript callbacks, including anything awaited, execute serially on the main thread. If a callback takes 500ms to run, all other pending callbacks are delayed by 500ms.
The three most common causes we see in production.
*1. Hot JSON paths.* JSON.parse and JSON.stringify are synchronous and surprisingly expensive on large payloads. A 10MB response body serialized on every request will peg the core during heavy traffic. Fix: validate payload size limits upstream, stream large responses.
*2. Regex backtracking.* Catastrophic backtracking on a poorly-written regex in a request validation middleware can turn a 100-byte input into seconds of compute. Fix: use a regex linter like safe-regex. Test your patterns with ReDoS checkers.
*3. Synchronous crypto.* crypto.pbkdf2Sync, crypto.scryptSync, and bcrypt sync variants are blocking. These are death on a busy server. Always use the async variants and ensure the callback doesn't defer to the main thread in a tight loop.
How to profile it. Run Node with --prof to generate a V8 tick profile, then use node --prof-process on the output. Alternatively, attach clinic.js flame for a visual flame graph. The hot function will be obvious — it's usually one frame consuming 80%+ of ticks.
What the cpum.ai agent looks for. When the agent sees a Node.js process pegged on a single core, it flags the event loop latency metric (if available via clinic-doctor or prom-client) and cross-references the PID's CPU time with the process start time to identify regressions introduced by recent deploys.