Field Note
EAGAIN is not a linker error
A cargo release build dies in the link step. The last three lines on stderr read:
terminate called after throwing an instance of 'std::system_error'
what(): Resource temporarily unavailable
error: linking with `cc` failed: signal: 6, SIGABRT
It looks like a linker bug. It is not. The linker is fine. The kernel refused to make another thread.
The shape of the lie
That stack of three lines reads as if something inside the link step crashed. terminate called after throwing is C++ stdlib language. std::system_error sounds like an error in the system being linked. signal: 6, SIGABRT looks like a crash. Three pieces of evidence all pointing at the toolchain.
All three lines are produced by the same event. A thread-creating call in lld (or any other multi-threaded child of the link step) failed. The runtime threw std::system_error. No handler caught it. The C++ stdlib's default terminate handler ran, printed the diagnostic, and called abort(), which is signal 6.
The diagnostic is correct. It is also misleading, because nothing about the message names the syscall that failed or the resource that ran out.
What "Resource temporarily unavailable" actually means
It means errno 11. EAGAIN. On Linux, every syscall that returns EAGAIN has a documented reason in its man page, and strerror(EAGAIN) renders it as "Resource temporarily unavailable" regardless of which syscall.
In a link step, the syscall that returned it is almost always clone() via pthread_create(). The man page for pthread_create spells it out: EAGAIN means "Insufficient resources to create another thread," and lists three sub-reasons.
- The soft RLIMIT_NPROC limit has been reached, counting all threads owned by the real user.
- The system-wide
/proc/sys/kernel/threads-maxceiling has been reached. - The kernel could not allocate the page tables for the new thread's stack.
(1) is by far the most common cause in containers. (2) shows up on long-running hosts with lots of zombie thread accounting. (3) means the host is genuinely out of memory and you would have noticed.
Why the linker, specifically
lld defaults to --threads=N where N is the core count visible to the process. On a 16-core host, the link step asks for 16 worker threads at once. Add rustc's codegen threads, build-script subprocesses, and whatever editor and watcher and dev server you have open, and the per-user thread count climbs.
Container runtimes usually inherit the host's RLIMIT_NPROC at process start, but they also impose their own pids cgroup limit (pids.max). Whichever is lower wins, and both are silent until a syscall hits them. The first syscall to fail is the one that asks for the thread that crosses the line.
It happens in the link step rather than the compile step because the link step is where lld decides to fan out. Compile threads are bounded by -j; link threads are bounded by core count and the linker's defaults.
How to confirm in 30 seconds
Three commands, in order:
ulimit -u # soft RLIMIT_NPROC for this shell
cat /proc/sys/kernel/threads-max
ps -L -u "$(id -u)" | wc -l # threads owned by this user right now
If the third number is close to the first or second, you have your answer. The check costs nothing and rules out the entire "linker bug" hypothesis class.
Inside a container, also check cat /sys/fs/cgroup/pids.max (cgroup v2) or the equivalent v1 path. That ceiling can be lower than your shell's ulimit -u and is invisible until a process tries to cross it.
The three fix shapes
Raise the limit. ulimit -u 65536 in the shell, or --ulimit nproc=65536:65536 on docker run, or set the cgroup's pids.max higher. This is the right move when the limit is genuinely too low for the work.
Lower the parallelism. Pass RUSTFLAGS='-C link-arg=-Wl,--threads=1' to make lld single-threaded. Slower link, but never starves. This is the right move on shared CI where raising the limit is not yours to do.
Shed other thread users. Stop the dev server, stop the watcher, close the editor's language servers. This is the right move when the link step is being squeezed by something incidental and the limit itself is fine.
The rule the failure teaches
When a C++ stdlib exception escapes from a tool, do not search the tool's source. Search the syscall. terminate / system_error / Resource temporarily unavailable is almost always a thread quota or file-descriptor quota miss, not a tool bug.
The same pattern fires for fork-bombed CI jobs, for Node processes that exhaust EMFILE on too many open files, for any tool that hits a per-user resource ceiling. The exception comes out the top of the stack labeled with the language's vocabulary, but the cause is one syscall deep, in the kernel's vocabulary.
The lookup chain that gets you to the answer fastest is short. Read the error literally. Find strerror's rendering of an errno value. Find the man page for the syscall that the failing tool would have called. Read the EAGAIN section. The fix shape almost writes itself once the resource is named.
Sources: pthread_create(3) · LLD threads documentation · proc(5) on threads-max · getrlimit(2) on RLIMIT_NPROC