Linux is finally rethinking fork() + exec() — and the numbers are brutal

4 min read 1 source explainer
├── "fork()+exec() is a legacy pattern that imposes avoidable costs on modern workloads and should be deprioritized in favor of posix_spawn() and io_uring alternatives"
│  └── jwilk (Hacker News (LWN article submission), 113 pts) → read

The LWN writeup of the LSF/MM/BPF Summit session argues that duplicating an entire virtual memory map via copy-on-write page tables only to tear it all down on exec() is the single biggest avoidable cost in process creation on modern Linux. Citing measurements from Meta and database vendors showing 5x-10x spawn latency reductions with posix_spawn() on large-memory parents, the piece frames fork() as elegant in textbooks but expensive in 2026 production workloads.

├── "The kernel community should treat the alternatives as first-class citizens, including a new io_uring spawn op"
│  └── LSF/MM/BPF Summit organizers (via LWN) (LWN.net) → read

Session organizers argue that posix_spawn() (2001) and vfork() already exist but have been treated as second-tier paths. The new willingness is to invest in making them first-class, including a proposed io_uring operation that lets a process queue a spawn alongside its I/O without ever entering the fork-and-tear-down dance — recognizing that modern async runtimes need spawn to integrate with their event loops, not sit outside them.

└── "The cost is workload-dependent — fork() is fine for small processes but punishing for large-memory parents like Node.js and JVM"
  └── top10.dev editorial (top10.dev) → read below

The editorial synthesis emphasizes that the page-table duplication cost is invisible on a shell with a 4KB heap but dominates profiles on a Node.js or JVM parent with a 4GB working set spawning git status in a loop. This reframes the debate as not about fork() being broken everywhere, but about it being a poor default for the specific class of large-memory, spawn-heavy modern applications.

What happened

At the 2026 Linux Storage, Filesystem, MM and BPF Summit, kernel developers spent a session arguing about something most application engineers assume was settled in 1979: how Linux creates new processes. The LWN writeup of the discussion lays out a case that `fork()` followed by `exec()` — the pattern at the heart of every shell, every `Runtime.exec`, every `subprocess.Popen` — is now the single biggest avoidable cost in process creation on modern Linux.

The specific complaint is mechanical. `fork()` duplicates the entire virtual memory map of the parent process, sets up copy-on-write page table entries for every mapping, and then `exec()` immediately tears all of it down to load a new binary. On a shell with a 4KB heap that's invisible. On a Node.js or JVM parent with a 4GB working set spawning `git status` in a loop, the page-table duplication alone shows up in profiles. The session's organizers cited internal measurements from large-scale users — including Meta and a couple of database vendors — where `posix_spawn()` cuts spawn latency by 5x to 10x on processes with non-trivial memory footprints.

The proposed alternatives aren't new. `posix_spawn()` has existed since 2001. `vfork()` is older. What's new is the willingness in the kernel community to treat `fork()` as a legacy path and invest in making the alternatives first-class — including a proposed `io_uring` op that would let a process queue a spawn alongside its I/O without ever entering the fork-and-tear-down dance.

Why it matters

The `fork()` model is one of those Unix ideas that's elegant in a textbook and expensive in production. It was designed when processes were small, page tables were small, and the kernel could plausibly copy a process's address space without anyone noticing. None of those assumptions survive contact with 2026 workloads.

The maintenance-cost framing matters here, because the kernel team isn't arguing `fork()` is broken — they're arguing it's an actuarial liability. Every new memory-management feature (transparent huge pages, memory tiering, CXL, KSM, userfaultfd) has to be taught how to behave correctly across a `fork()` boundary, and every one of those teachings is a recurring tax on the MM subsystem. That's the move-beyond argument: not that `fork()` is slow today, but that keeping it cheap will cost more than replacing it.

The `io_uring` angle is where this gets genuinely interesting. The kernel already has a high-throughput, batched syscall interface that application servers are increasingly built around. Adding spawn to `io_uring` means a Postgres-style worker pool, or a web server farming out short-lived helpers, can submit "create process, redirect these FDs, run this binary" as a single SQE and pick up the PID off the CQ. No fork. No exec dance. No signal-handler reset minefield. The semantic gap between "do I/O" and "do process" finally closes.

The pushback in the LWN comments is predictable and partly correct. `posix_spawn()` has an awkward API — it expresses pre-exec setup as a `file_actions` object instead of arbitrary code, which means anything you used to do between fork and exec (sophisticated FD remapping, namespace setup, seccomp filter installation) needs a kernel-supported primitive. Languages whose subprocess libraries already use `posix_spawn()` under the hood (recent glibc, modern Python, Go's `os/exec` on Linux when it can) get the speedup for free. Languages that hand-roll fork/exec for control — including a lot of init systems, container runtimes, and anything doing PAM — don't.

A shell developer in the thread put it bluntly: bash, dash, and zsh all rely on the ability to run arbitrary code between fork and exec for redirections, job control, and variable expansion. Moving shells off `fork()` is not a recompile — it's a redesign of how shells think about subprocess setup. That's a multi-year migration, and shells are not going to lead it.

What this means for your stack

If you write application code, the practical implications are immediate and small. Check whether your runtime already uses `posix_spawn()`: Python 3.8+ does on Linux when safe, Node.js does via libuv when the parent has no signal handlers blocking it, Java 11+ does on most distros. If you're forking from a process with a multi-gigabyte heap — JVM services spawning sidecars, Node servers calling out to CLI tools, Python data pipelines invoking `ffmpeg` — you may already be paying the `fork()` tax and not know it. Profile with `perf` or `bpftrace` on `copy_page_range` and you'll see it instantly.

If you maintain infrastructure code, the calculus is harder. Container runtimes, init systems, and anything doing fine-grained namespace or seccomp setup between fork and exec will need to track the new kernel primitives carefully — the migration story is real work, not a config flag. Watch the `clone3()` and `io_uring` spawn proposals; the kernel folks are explicit that they want to make those primitives expressive enough that runc, systemd, and friends can move off `fork()` without losing capability.

For everyone else, the medium-term takeaway is that the cost of `subprocess.run` in a large parent is about to drop by an order of magnitude in some configurations. That changes the design tradeoff between "spawn a helper" and "link a library." Patterns that were too expensive — per-request subprocess invocation, fan-out parallelism via OS processes instead of threads — become viable again. If you've been carrying around a connection pool or a worker pool specifically to avoid the spawn tax, the math is going to shift under you within a kernel release or two.

Looking ahead

The Linux kernel almost never deprecates a syscall, and `fork()` is not going to be deprecated. What's actually happening is more interesting: the kernel community is quietly building a parallel process-creation API that's faster, cleaner, and better-integrated with the rest of the modern Linux ABI — and signaling to userspace that the smart money is on migrating. The shells will be last. The application runtimes will be first. The interesting question is which language ecosystem ships the first idiomatic `io_uring`-spawn API and turns subprocess invocation back into a primitive worth using.

Hacker News 338 pts 324 comments

Moving beyond fork() + exec()

→ read on Hacker News

// share this

// get daily digest

Top 10 dev stories every morning at 8am UTC. AI-curated. Retro terminal HTML email.