Article View

Scroll down to read the full article.

The Phantom EMFILE/EAGAIN: Node.js Child Processes and Cgroup v1's Hidden Traps

calendar_month July 27, 2026 |
Quick Summary: Troubleshoot frustrating Node.js ChildProcess.spawn EMFILE/EAGAIN errors in cgroup v1 container environments. Uncover hidden pids.max and ulimit m...

Another EMFILE? Seriously? You checked ulimit -n, right? It's 'unlimited' or some ridiculously high number. The container orchestrator reports everything's green. Your Node.js application, usually a bastion of stability, is sporadically crashing, spewing 'Too many open files' or 'Resource temporarily unavailable' errors, but the usual suspects are clean. Welcome, my friend, to the infuriating, obscure world of cgroup v1's subtle child process limits.

This isn't your average 'forgetting to close a file descriptor' problem. This particular beast manifests when your high-concurrency Node.js application, especially one that spawns numerous short-lived child processes (think image processing with ImageMagick, heavy git operations, or FFmpeg encoding), hits a wall. A wall you can't see with standard diagnostic tools. It works perfectly in development, on your VM, even on some older staging environments. Then it ships to production, runs in a containerized cgroup v1 environment, and starts intermittently falling over. Pure pain.

The Problem: Node.js ChildProcess.spawn Fails

Your Node.js logs are littered with:

  • Error: spawn ENFILE
  • Error: spawn EAGAIN

These errors typically occur under peak load, when your application is attempting to fork more child processes than the system (or more accurately, the cgroup) believes it's allowed. The frustrating part is that traditional ulimit -n checks on the host machine will show high limits, making you tear your hair out.

Tangled knot of server cables and glowing optical fibers in a dark
Visual representation

Triggering Environments

This particular brand of hell seems to thrive in specific older setups. Here's where we've seen it bite hardest:

Operating System Kernel Version Range Node.js Version Range Container Runtime Observed Symptoms
Red Hat Enterprise Linux 7.x (e.g., 3.10.0-862 to 3.10.0-1160) 12.x, 14.x, 16.x Docker (cgroup v1), Podman (cgroup v1) EMFILE/EAGAIN on spawn
CentOS 7.x (e.g., 3.10.0-862 to 3.10.0-1160) 12.x, 14.x, 16.x Docker (cgroup v1), Podman (cgroup v1) EMFILE/EAGAIN on spawn
Ubuntu 16.04, 18.04 (with specific cgroup setups) 12.x, 14.x, 16.x Docker (cgroup v1) EMFILE/EAGAIN on spawn

Debugging Steps (Stop Screaming, Start Digging)

1. "You checked ulimit -n already, right?"

Yes, you did. And it showed 'unlimited' or some value like 65535. This is where most people give up, utterly baffled. The problem? That's the host's ulimit. Your container, specifically its cgroup, might be operating under entirely different, much stricter limits.

2. Inside the Container: /proc/<pid>/limits

This is your first real step. Get into the running container (docker exec -it <container_id> bash) and find your Node.js process PID. Then, inspect its limits:

cat /proc/$(pgrep node)/limits

Look specifically for Max open files and Max processes. You might find Max open files is still high, but Max processes is surprisingly low. Or, even if Max open files is reported high, the cgroup might be preventing the child process itself from allocating its stdin/stdout/stderr.

A tiny
Visual representation

The Root Cause

The fundamental architectural flaw lies in the interaction between older Linux kernel cgroup v1 implementations (particularly on RHEL/CentOS 7 series) and how process managers (like Docker) propagate or default resource limits. Specifically, the pids.max controller in cgroup v1, which limits the total number of processes and threads a cgroup can spawn, defaults to a laughably low value (often 500-1000) if not explicitly set by the orchestrator. This is particularly problematic for applications that follow a microservices architecture and frequently fork. For a deeper dive into such scaling challenges, you might want to review Architecting for Hypergrowth: The Brutal Realities of Scaling FAANG-Level Distributed Systems.

Furthermore, the nofile (open files) ulimit, while appearing high at the host level, can be subtly constrained or mismanaged within cgroups, particularly for newly spawned child processes inheriting the cgroup context. Node.js applications that heavily rely on ChildProcess.spawn rapidly deplete this pids.max allowance, leading to EAGAIN (resource temporarily unavailable) errors. The EMFILE (too many open files) variant occurs when the child process itself, even if briefly, cannot allocate its standard file descriptors because its inherited nofile limit within the cgroup context is exhausted or misconfigured. Understanding these underlying system interactions is crucial when dealing with complex distributed systems, a topic explored further in The Crucible of Scale: Deconstructing FAANG's Distributed Systems Architecture.

The Solution: Explicit Cgroup Limits

Stop trusting defaults. Stop relying on host-level ulimit. You need to explicitly tell your container runtime what limits to enforce for the cgroup. For Docker, this means using --pids-limit and --ulimit nofile.

Here's a complete, copy-pasteable example for a docker run command. Adjust values to your needs, but start generously if you're hitting these issues. Remember, 4096 pids is often a good baseline for busy Node.js apps, and 65536 is a standard high nofile limit.

docker run \
--name your-node-app \
--rm \
-p 8080:8080 \
--pids-limit 4096 \
--ulimit nofile=65536:65536 \
your-node-image:latest
  • --pids-limit 4096: This sets the pids.max for the container's cgroup to 4096. This means the container (and all processes/threads within it) can have up to 4096 active process IDs. Adjust higher if you still see EAGAIN under extreme load.
  • --ulimit nofile=65536:65536: This explicitly sets the soft and hard RLIMIT_NOFILE (max open files) for processes within the container. While your host might show it as 'unlimited', ensuring it's explicitly high here can prevent the subtle EMFILE for child processes.

Final Thoughts

Cgroup v1 is a messy beast, and its interaction with process limits, especially for frequently forking applications, is a common source of obscure production issues. Don't just trust ulimit -n on the host. Look inside the damn container with /proc/<pid>/limits. Always explicitly define your resource limits when dealing with containerized workloads, especially on older kernel versions. This isn't just about memory and CPU; PIDs and open file descriptors are critical too.

Discussion

Comments

Read Next