The LWN piece revisits Microsoft Research's 2019 'A fork() in the road' paper and argues that copying page tables for a process that will immediately exec() is overhead the kernel can no longer pretend is free. It cites measured fork() latencies scaling linearly into tens of milliseconds for large-heap server workloads like JVMs, Postgres backends, and Ruby application servers.
The article points to posix_spawn() optimization work landing in glibc and active proposals to expose richer process creation through io_uring submission queues as the pragmatic migration path. The framing 'mostly yes, but kill it gently' reflects the consensus that fork() should be phased out incrementally rather than removed outright.
The piece emphasizes that most developers never notice fork() cost because most processes are small, but the systems 'writing the bills' — Postgres forking per connection, Python multiprocessing defaulting to fork on Linux, Ruby application servers — are precisely where the overhead dominates. It notes macOS already switched Python's default to 'spawn' for analogous reasons involving runtime state corruption.
LWN published a long-form piece on the renewed push to deprecate fork()+exec() as Linux's primary process-creation primitive. The discussion picks up threads from recent kernel mailing list debates, posix_spawn() optimization work landing in glibc, and active proposals to expose richer process creation through io_uring submission queues. The argument: in a world where the overwhelming majority of fork() calls are immediately followed by exec(), copying the entire address space — even with copy-on-write semantics — is overhead the kernel can no longer pretend is free.
The specific complaints are well-documented and a decade old. fork() on a process with a multi-gigabyte heap has to duplicate page tables, which costs both memory and CPU proportional to the parent's working set. On systems running large JVMs, Postgres backends, or Ruby application servers, the latency of fork() is dominated not by anything the new process needs, but by bookkeeping for memory the child will throw away the moment exec() runs. Microsoft Research's 2019 paper *A fork() in the road* — which the LWN piece revisits — measured fork() costs scaling linearly with parent address space size into the tens of milliseconds for realistic server workloads. That paper called fork() "an anachronism," and the Linux community spent the next half-decade arguing about whether they were right.
The answer that's quietly emerging is: mostly yes, but kill it gently.
Most developers never think about process creation cost because most processes are small. But the systems where fork() costs matter — database servers, language runtimes, language servers, build systems — are the ones writing the bills. Postgres still uses fork() for every incoming connection. Python's multiprocessing module uses fork() by default on Linux (the default switched to "spawn" on macOS for reasons involving Objective-C runtime state that turned out to rhyme with the Linux problems). Ruby's process model has spent a decade trying to dodge fork() pitfalls inside JIT'd code. Every one of these hits the same wall: fork() copies state you don't want and can't easily opt out of.
The race conditions are arguably worse than the raw cost. Between fork() and exec(), the child inherits every file descriptor, every signal handler, every mmap'd region, and any thread the parent had — and almost none of that is what you wanted to copy. The standard userspace workaround is to walk `/proc/self/fd` and close everything you don't need, which is racy in multithreaded programs because another thread can open a descriptor between your scan and your close. posix_spawn() was the POSIX committee's attempt to fix this with explicit file-action descriptors specified up front, but glibc's posix_spawn() was, until recently, implemented as a thin wrapper over fork()+exec() — so users got the awkward API and none of the performance.
That finally changed. Recent glibc versions implement posix_spawn() via clone() with CLONE_VFORK and CLONE_VM, which avoids the page-table copy entirely and runs in roughly constant time regardless of parent size. Benchmarks circulating on the kernel list show 10-100× improvements for processes with large heaps. Python 3.14 (and recent 3.13 point releases) now use posix_spawn() for multiprocessing where possible, finally giving Python on Linux the same performance characteristics as Python on macOS — and incidentally fixing a class of locking bugs that had been wontfix'd for years.
The deeper objection — the one that genuinely splits the kernel community — is whether fork() should be replaced or just augmented. The "replace it" camp points to the security record: fork() bugs have produced CVEs every few years, including the recent OpenSSH regression where signal handler races in the pre-auth fork path were exploitable. The "augment it" camp has a real argument too. fork() is one of Unix's last truly composable primitives: any program can spawn any other program with no new abstraction needed. Throwing that out for a kitchen-sink syscall risks reinventing Windows' CreateProcess(), which has twelve parameters, a STARTUPINFO struct, and an opinion about every one of them.
io_uring is the wildcard. There are active proposals to expose process creation through io_uring submission queues, which would let a single submitted operation both spawn a process and set up its file descriptor table atomically, with no userspace race window between fork() and the close-fds dance. This is the closest thing to a clean replacement on the horizon, but it ties process creation to a Linux-specific async I/O interface, which makes BSD and macOS porters nervous. It would not be the first time Linux solved a portability problem by becoming less portable.
If you run Postgres at scale, the connection-establishment cost is already on your radar; the fork() per connection is one reason pgBouncer exists. Connection pooling will continue to matter regardless of what the kernel does, but the gap between "fork on demand" and "pre-forked worker pool" narrows considerably if posix_spawn() lands across the database ecosystem. Worth watching the pgsql-hackers list for any patches that move connection setup off fork() — the discussion has been intermittent for years but the upstream glibc work changes the math.
If you ship Python, audit any code that uses `multiprocessing` with the "fork" start method. The default is changing — Python 3.14 will likely warn on fork() in multithreaded processes, mirroring macOS's behavior since 3.8. If your code assumes file descriptors and locks survive a fork, that assumption was always broken; it's about to start crashing loudly instead of silently corrupting data. Move to "spawn" or "forkserver" now and you'll skip the migration pain. The cost is one-time pickling overhead at process start, which is invisible next to the latency you save on every subsequent spawn.
If you maintain a language runtime, the calculus is shifting against you if you still document fork-safe semantics. Go has never exposed fork() in the standard library and used posix_spawn-equivalent primitives from day one; that decision is aging well. Rust's `std::process::Command` uses posix_spawn() where available. Languages that exposed fork() as a user-facing primitive — Perl, Ruby, Python — are all quietly walking it back. If you're designing a new runtime in 2026 and you're still considering fork() as a public API, the answer is no.
The fork()-vs-posix_spawn() debate has been going on long enough that "kill fork()" has become a meme, but the actual trajectory is more boring and more likely than either side claims: fork() stays in the kernel, posix_spawn() becomes the recommended path for everyone who would have written fork()+exec(), and the small set of legitimate fork()-without-exec() use cases — pre-forked workers, copy-on-write debuggers, a few tracing tools — get explicit kernel support that doesn't have to apologize for the rest of the API surface. io_uring process creation lands eventually with caveats, and the Linux-only crowd gets one more reason to be Linux-only. The fight over the primitive is mostly over. The fight over what to do with the sixty years of production code written against it is just starting.
Top 10 dev stories every morning at 8am UTC. AI-curated. Retro terminal HTML email.