Article View

Scroll down to read the full article.

The uv_async_send Spectre: Why Your node-term-sync Addon Crashes on Legacy Linux (and How to Kill It)

calendar_month July 30, 2026 |
Quick Summary: Node.js `uv_async_send` queue exhaustion on older Linux kernels with C++ addons. Troubleshoot `SIGWINCH` crashes in `node-term-sync` applications.

A chaotic tangle of glowing wires and circuits
Visual representation

Alright, listen up. We've all seen our fair share of Node.js applications inexplicably dying. Usually, it's memory leaks, unhandled promises, or some third-party package pulling in a rogue dependency. Standard fare. But sometimes, just sometimes, you hit something so utterly bizarre, so ridiculously specific, you wonder if you’ve angered an old god of forgotten Unix utilities.

This is one of those times. If you're running a Node.js application that uses a custom C++ N-API addon – specifically one that interacts with terminal dimensions – and it's sporadically crashing with an assertion failure related to uv_async_send queue exhaustion, you're in the right place. And you're probably already incredibly frustrated.

The Problem: uv_async_send Queue Full

The symptom is brutal and immediate. Your Node.js process, chugging along happily, suddenly segfaults or exits with a message about Assertion 'uv_async_send queue full' failed. No stack trace pointing to your JavaScript code. Nothing useful from V8. Just a hard exit, often traced back to libuv's internals. It's infuriating.

You’ll notice this isn't happening under normal load. Your CI/CD pipeline passes. Your staging environment, running newer kernel versions, is fine. It’s only when you deploy to that specific, older production box, or when a developer is testing on an ancient VM, that it crops up. And then, the kicker: you can reliably trigger it by frantically resizing the terminal window running the Node.js process.

Initial Head-Scratching

We’ve been down this road. You suspect memory corruption in your C++ addon. You run Valgrind. You sprinkle console.log everywhere. You spend days dissecting core dumps. Nothing points to a clear error in your custom C++ logic. The Node.js event loop seems to be processing fine. You check for runaway loops, tight CPU binds – nope. Everything seems to be operating within reasonable bounds, until it just… doesn't.

The problem isn't about the raw speed of your event loop's ability to process general tasks, as you might optimize for in Engineering Sub-Millisecond Algorithmic Execution. This is a very specific bottleneck.

The Specific Environment Where This Error Triggers

This is crucial. This isn't a universal bug. This is environmental.

Operating System Kernel Version Node.js Version Affected Addon Trigger Condition
Ubuntu Server 4.4.x - 4.15.x 14.x, 16.x node-term-sync (or similar N-API addon relying on SIGWINCH) Rapid terminal resizing
Debian Jessie/Stretch 4.x 14.x, 16.x node-term-sync Rapid terminal resizing

Note: Newer Linux kernels (5.x+) and Node.js versions (18+) tend to mitigate this behavior, either due to kernel-side signal coalescing improvements or libuv’s increased robustness.

The Hunt: Identifying SIGWINCH

After much gnashing of teeth and strace output analysis, the pattern emerges: the crash consistently occurs after a flurry of SIGWINCH signals. Your node-term-sync addon, being the good citizen it is, registers a handler for SIGWINCH to update Node.js about terminal dimension changes. It typically uses uv_async_send to safely dispatch these updates to the Node.js event loop from a signal handler context.

But here's the rub: SIGWINCH can fire *very* rapidly during aggressive terminal resizing. We're talking hundreds per second, potentially. And on these older kernel/Node.js combinations, libuv's internal queue for uv_async_send, while typically sufficient, gets overwhelmed.

The Root Cause

The underlying architectural flaw isn't a bug in libuv itself, nor necessarily a direct flaw in your addon's C++ logic. It's a fundamental mismatch between the aggressiveness of SIGWINCH delivery on specific older Linux kernels and the default buffering capacity of libuv's internal uv_async_send queue when dealing with extremely high-frequency, unthrottled signal events. Modern kernels often implement signal coalescing, or newer libuv versions have increased internal queue sizes or more sophisticated handling. On the affected systems, each SIGWINCH triggers a synchronous call to uv_async_send, and if the Node.js event loop can't drain that queue fast enough – perhaps because it's busy with other I/O, or just not scheduled to run immediately – the queue overflows, leading to the assertion failure. It's a nasty edge case where raw OS signal frequency meets event loop design, impacting performance critical addons that demand Sub-Microsecond Supremacy.

A detailed
Visual representation

The "Aha!" Moment

The problem is too many SIGWINCH signals, too quickly, hitting a constrained queue. The addon isn't inherently broken; the environment is just too enthusiastic with signal delivery for that specific libuv version’s default capacity. The critical insight is realizing that for many applications, *every single* SIGWINCH doesn't need to be immediately processed. A slight delay or debouncing is perfectly acceptable.

The Fix: A "Dumb" Terminal Workaround

For a quick, field-deployable fix on affected systems, especially where modifying the C++ addon immediately isn't feasible, we can trick the environment. By telling the Node.js process it's running in a much simpler terminal type, we can sometimes cause the system or the terminal emulator to be less aggressive in sending SIGWINCH, or even cause some layers to ignore it. This isn't a permanent solution for the addon, but it stops the bleeding.

Launch your Node.js application with a "dumb" TERM environment variable. This often signals to terminal emulation layers (and sometimes even the kernel) that advanced terminal features, including aggressive window resizing notifications, aren't necessary or supported.

TERM=vt100 node your-app.js

Yes, it feels hacky. Yes, it might impact other terminal-dependent features if your application relies heavily on complex ANSI escape codes. But for stopping those crashes cold, this is a shockingly effective, low-effort mitigation. For diagnostic purposes, you can also try TERM=linux or TERM=dumb.

The Permanent Solution (Addon Patch)

The proper fix, of course, is to modify your node-term-sync C++ addon. Implement a rate-limiter or a debouncer for the SIGWINCH handler *before* it calls uv_async_send. This ensures that even if you receive 200 SIGWINCH signals in a single millisecond, you only dispatch, say, one uv_async_send event every 50ms or 100ms. This keeps the libuv queue happy.

  • Implement a timer: When a SIGWINCH is received, set a flag and arm a short timer. If another SIGWINCH comes before the timer fires, simply reset the timer. Only dispatch uv_async_send when the timer finally fires.
  • Atomic counter with threshold: Increment an atomic counter on each SIGWINCH, but only trigger uv_async_send if a certain period has passed since the last actual dispatch, or if the counter exceeds a certain value.

Final Thoughts

This kind of obscure, environmental issue is why SRE exists. It's not always about algorithmic efficiency or scaling databases. Sometimes, it's about the subtle, painful interactions between older kernel signal handling, specific libuv versions, and overzealous C++ addons. Understand your stack, down to the metal, and don't be afraid to try seemingly nonsensical workarounds when debugging the truly weird stuff.

Discussion

Comments

Read Next