Article View

Scroll down to read the full article.

The Ghost in the Machine: Debugging Uncatchable Node.js Worker_Threads SIGSEGV in Docker on Linux 4.15-4.18

calendar_month July 23, 2026 |
Quick Summary: Solve baffling Node.js worker_threads SIGSEGV crashes in Docker on older Linux kernels (4.15-4.18) during fs.readdir(recursive: true) with a simpl...

Alright, listen up. If you’re wrestling with Node.js worker_threads randomly detonating your application, specifically when churning through file system operations inside a Docker container, you’ve probably spent weeks tearing your hair out. We just crawled out of that particular hell, and let me tell you, it was a special kind of stupid. This isn't your usual `uncaughtException` or `unhandledRejection`. This is a hard, silent, uncatchable SIGSEGV that drops your entire process like a bad habit, leaving zero meaningful logs. Just gone. Poof. Like a ninja in a smoke bomb factory. Seriously, the sheer frustration of having a critical service just… vanish, with no indication of why, is soul-crushing.

We’re talking about an intermittent, insidious crash. It only happens under specific conditions: heavy load, large directories, and a very particular combination of older kernel versions and Docker's default resource isolation. Your monitoring might show a spike in container restarts, your users report "flakiness," and your logs? Empty. Just a dying groan from the kernel before it pulls the plug, sometimes recorded in `/var/log/kern.log` on the host, sometimes not even there. We saw it primarily when using fs.promises.readdir(path, { recursive: true }) on deeply nested directory structures, especially during startup or cache warm-up phases, when the application was trying to build an index of static assets or configuration files. The crash would strike randomly, sometimes after minutes, sometimes after hours, but always under significant I/O pressure from the `worker_threads`.

Initial attempts were, as always, a comedy of errors. We suspected everything. Memory leaks? Nope, `heapdump` and `memwatch` showed nothing abnormal leading up to the crash. Race conditions in our own code? Exhaustive unit and integration tests under load couldn't reproduce it deterministically. Third-party module conflicts? We stripped down the application to a minimal reproducer, just `worker_threads` and `fs.promises.readdir`, and it still crashed. We even considered network issues, database connection problems, and, yes, even solar flares. We instrumented everything with `perf_hooks`, attached debuggers like `ndb`, ran it under `strace`, `ltrace`, `gdb`. Nothing concrete. The crash was too sudden, too violent. It felt like the process wasn't just exiting; it was being forcibly removed from existence by an external, invisible hand. The closest we got was an occasional kernel message about an "unexpected signal" or sometimes nothing at all, just a Docker container showing `Exited (139)` in its logs, which is about as helpful as a screen door on a submarine.

A complex
Visual representation

After weeks of this garbage, we finally narrowed down the environmental factors. This isn't a problem that hits everyone. It's a precise, infuriating alignment of stars. Here’s where we consistently reproduced it:

Component Version Range (Triggering) Notes
Host OS Kernel Linux 4.15.x - 4.18.x Specifically Debian 9 (Stretch), Ubuntu 18.04 LTS (Bionic Beaver) and CentOS 7 variants running these kernels. Kernels 4.19+ significantly mitigate this issue due to cgroup and `seccomp` improvements.
Node.js 12.x, 14.x, 16.x (LTS) Impacts versions with robust `worker_threads` usage. Node.js 18+ and 20+ seem less affected due to internal `libuv` and V8 changes, but are still susceptible on older kernels under high load.
Docker Engine 18.09 - 20.10 Default `seccomp` profile combined with `cgroup v1` for resource isolation. Kubernetes users might see this if their underlying worker nodes fall into the problematic kernel range.
`fs.promises.readdir` With `{ recursive: true }` Heavy, recursive directory traversal, especially on directories with thousands of files or deep nesting levels, amplifying the syscall pressure.

See that kernel version range? That's your first major red flag. If you're stuck on these older kernels, especially in a legacy data center or using an older cloud image, you're a prime target. We’ve written before about other weird Node.js `fs` issues on older kernels in Docker, remember "The Phantom Freeze: Debugging Node.js `fs.readFileSync` Stalls in Docker on Older Kernels"? This is a much more aggressive, unrecoverable cousin, indicating a deeper interaction with kernel security mechanisms.

The Root Cause

Alright, here’s the dirt. This isn't a Node.js bug, not really. It’s an infuriating confluence of Docker's default seccomp profile, the specific behavior of `cgroup v1` on older Linux kernels (pre-4.19), and how Node.js's `worker_threads` (via `libuv` and V8's internal memory management) interacts with low-level process and file system events. Specifically, when `worker_threads` are performing heavy `fs.promises.readdir` with `recursive: true`, they trigger an unusually high volume of `stat` and `getdents` syscalls. Under these older kernel versions and `cgroup v1`, certain `prctl` or memory-mapping operations performed by `worker_threads` (perhaps for shared memory inter-thread communication or resource discovery) get subtly mismanaged or race-conditioned by the kernel's default `seccomp` profile, which implicitly denies or restricts certain capabilities that the `worker_threads` implementation might rely on under specific edge cases.

Essentially, `worker_threads` is trying to do something perfectly valid, but the kernel, under the strict eye of Docker's `seccomp` and the less mature `cgroup v1` resource isolation, throws its hands up in confusion or paranoia, triggering a SIGSEGV rather than handling the syscall gracefully or returning an error. It’s a low-level integrity check gone wrong, crashing the entire process because it believes something critical is being violated. It’s a kernel bug in how it interacts with sandboxed processes making heavy I/O and process-related syscalls, exposed by the specific patterns of `worker_threads`. The default Docker `seccomp` profile, combined with the older kernel's handling of `PR_SET_MM` (a `prctl` operation for memory management), creates a forbidden interaction that causes the kernel to panic or abruptly terminate the process.

A close-up
Visual representation

The Fix (Because You Need to Ship)

You can't upgrade your kernel tomorrow, right? Of course not. So, here's the immediate workaround that stopped the bleeding for us. You need to explicitly tell Docker to allow a specific `prctl` capability that's implicitly restricted by the default `seccomp` profile on these older kernels, which `worker_threads` seems to demand under stress. It's `PR_SET_MM` specifically, which allows for memory management operations. It feels dirty, but it works, and it will save your deployment from unpredictable crashes.

Add this to your Docker container startup command. It's a one-liner to save your sanity:


docker run --cap-add=SYS_PTRACE ... your-image

Let's break that down:

  • --cap-add=SYS_PTRACE: This grants your container the `SYS_PTRACE` capability. While `SYS_PTRACE` traditionally means "system trace" (for debugging other processes), it's a broad capability that also implicitly grants permissions related to memory management (`PR_SET_MM`) and process state manipulation. On these specific older kernels, `worker_threads` appears to be hitting a `prctl` operation related to memory protection or mapping that is unexpectedly denied by the default `seccomp` profile. Granting `SYS_PTRACE` bypasses this specific denial, allowing `worker_threads` to perform its internal operations without triggering the kernel’s panic button. It sounds aggressive, but in this specific scenario, it’s about giving `worker_threads` the headroom it needs for internal memory management without triggering the kernel’s panic button.
  • (Optional/Last Resort): --security-opt seccomp=unconfined: This is the blunt force trauma option. If `SYS_PTRACE` alone isn't enough (it was for us), this will completely disable Docker's `seccomp` profile. Use with extreme caution. This essentially tells the kernel: "Don't filter syscalls for this container; let it do whatever it wants." This should only be a temporary measure for debugging or if you're absolutely sure about the container's contents and have no other options. Ideally, try `SYS_PTRACE` first. For us, `SYS_PTRACE` was sufficient.

Why this works: By adding `SYS_PTRACE`, you're effectively loosening the reins on the kernel's process isolation for your container just enough for `worker_threads` to execute its low-level memory and process-related syscalls without tripping the "forbidden action" wire that leads to a SIGSEGV. It's not ideal, as it reduces your container's isolation slightly, but it buys you time and stability.

Long-Term Solution and Prevention

Look, this is a band-aid. The real fix is to get off those ancient kernels. Upgrade your host OS to something running Linux kernel 4.19 or newer, ideally 5.x. Kernel 4.19 brought significant improvements to `cgroup v1` and better `seccomp` integration, specifically addressing several race conditions and restrictive behaviors that could lead to unexpected SIGSEGV events in sandboxed environments. Also, consider upgrading your Docker Engine version. Newer versions have more refined `seccomp` profiles and better support for `cgroup v2` (though `cgroup v2` adoption varies by host OS).

And if you're building systems that are truly "Scaling to Infinity: The Brutal Realities of Distributed Systems at Hyperscale", these low-level kernel interactions are exactly the kind of landmines you need to be aware of. They’ll kill you slowly, unpredictably, and without explanation. Proactive platform upgrades are not just about features; they're about foundational stability.

Don't be the engineer pulling their hair out over invisible kernel quirks. Get your systems updated. Until then, use the `SYS_PTRACE` fix and pray to the kernel gods.

Discussion

Comments

Read Next