The authors argue fork() made sense for a single-threaded PDP-11 in 1975 but has been a liability ever since. They contend its semantics are fundamentally incompatible with modern multi-threaded, large-memory processes and that the entire model needs to be retired in favor of explicit process-creation primitives.
The LWN piece frames the kernel community's posture as pragmatic: nobody will remove fork(), but nobody will recommend calling it directly either. The replacement is happening incrementally through clone3(), pidfd, posix_spawn() optimizations, and io_uring spawn primitives — a slow dismantling rather than a dramatic API break.
The editorial argues that despite copy-on-write, fork() remains O(n) in process memory because page tables must still be duplicated — roughly half a millisecond per megabyte per the Baumann measurements. Redis is cited as the canonical example: a 100GB instance can stall for multiple seconds during BGSAVE entirely because of fork()'s page-table cloning overhead.
LWN's latest piece, *Moving beyond fork() + exec()*, surfaces an ongoing kernel-side conversation that has been simmering for years: the canonical Unix process-creation pattern is being quietly dismantled, one syscall at a time. The arguments aren't new. Andrew Baumann, Jonathan Appavoo, Orran Krieger, and Timothy Roscoe published *A fork() in the road* at HotOS 2019 and called fork() "an architectural anachronism" — a primitive whose semantics made sense for a single-threaded PDP-11 in 1975 and have been a liability roughly ever since.
What's actually changed in the last six years is the infrastructure. `clone3()` landed in Linux 5.3 with a structured, extensible argument format. `pidfd` followed, giving callers a file-descriptor handle on a process instead of a racy integer PID. glibc's `posix_spawn()` has been progressively optimized to the point where it's now genuinely faster than `fork()` + `exec()` on Linux for most workloads. io_uring has been gaining spawn-adjacent primitives. The pattern that defined Unix process creation is being replaced not by a single dramatic API, but by attrition.
The LWN discussion captures the kernel community's pragmatic posture: nobody is going to remove `fork()`. But nobody is going to recommend you call it directly either.
The technical case against `fork()` is well-rehearsed but worth restating because most application developers have internalized the pattern without internalizing the cost.
First, `fork()` is O(n) in process memory even with copy-on-write. The kernel still has to duplicate the page tables, and on a process with a 100GB resident set, that page-table walk is not free. The Baumann paper measured roughly half a millisecond per megabyte; Redis users have been warning about this for over a decade. A Redis instance with a 100GB working set can stall for multiple seconds during BGSAVE — entirely because fork() has to clone its page tables.
Second, `fork()` is actively hostile to multithreaded programs. Only the calling thread survives in the child. Every mutex held by another thread at the moment of `fork()` is now held by a thread that doesn't exist, in a process that can never release it. `pthread_atfork()` exists to paper over this. It does not actually work — it cannot, because async-signal-safety constraints between `fork()` and `exec()` are nearly impossible to satisfy across a real codebase.
Third, `fork()` implicitly duplicates every resource the parent had: file descriptors, signal handlers, memory mappings, locks, the TLS arena, the malloc heap. The child almost never wants any of this. The classic pattern is `fork()` → close everything you don't need → reset signals → `exec()` — a sequence that is wrong in most codebases and silently wrong in many.
The alternatives are not theoretical. `posix_spawn()` has been in POSIX since 2001 and is now the default backend for Python's `subprocess` (3.8+), Node.js's `child_process` (13+), and most JVM `ProcessBuilder` implementations. Go has never used `fork()` — it goes straight to `clone()` with the flags it actually wants. `clone3()` with `CLONE_PIDFD` gives you race-free process management; you can `poll()` on a pidfd and know without ambiguity when your child died. FreeBSD has `pdfork()`. macOS has been pushing `posix_spawn()` aggressively for years.
If you're writing application code in 2026, the practical takeaways are narrower than the kernel discussion suggests, but they matter.
If you're maintaining a large-process daemon — PostgreSQL, Redis, anything with a multi-gigabyte in-memory state — your fork() costs are not theoretical and they directly land in your tail latency. PostgreSQL forks a backend per connection, which is why pgbouncer exists. Redis forks for RDB snapshots and AOF rewrites, which is why every Redis operator learns to schedule BGSAVE during off-peak windows. If you've never measured `fork()` latency on your production process size, do that this week. The number will surprise you.
If you're calling `subprocess.Popen` in Python, `child_process.spawn` in Node, or `ProcessBuilder` in Java, you are almost certainly already on `posix_spawn()`. Check your runtime version. The performance difference on a fat parent process is two to three orders of magnitude.
If you're writing a supervisor, a container runtime, or anything that needs to know when a child died: stop using PIDs. Use `pidfd_open()` on Linux, `kqueue` + `EVFILT_PROC` on the BSDs. The PID-reuse race is a real bug class — systemd has shipped CVEs for it.
Do not call fork() without an immediate exec() in any multithreaded program. This is not a style preference; it is a correctness requirement that the C and C++ standards both acknowledge but cannot enforce. If you need to launch a subprocess from a threaded program, `posix_spawn()` is the only safe answer. If you need to clone state without exec'ing, you don't actually need `fork()` — you need a worker pool, or threads, or async, or anything else.
The transition will be slow, unglamorous, and largely invisible to the developers who benefit from it. Kernel maintainers will keep adding primitives — io_uring's spawn operations are the next obvious frontier, because they let you launch a process without a separate syscall round-trip at all. glibc will keep optimizing `posix_spawn()`. Language runtimes will keep migrating their child-process backends.
The interesting open question isn't whether fork() goes away — it won't — but whether POSIX will ever standardize the post-fork() world, or whether each kernel diverges into its own incompatible set of clone/spawn/pidfd primitives. Linux is already well ahead with `clone3()` and `pidfd`. FreeBSD has `pdfork()`. macOS has its own preferred surface. The cross-platform Unix abstraction that `fork()` provided — for all its flaws, it ran identically on every Unix since V6 — may be the real casualty here. For most application developers, that's fine. For anyone shipping a portable systems library, it's the next decade of work.
Top 10 dev stories every morning at 8am UTC. AI-curated. Retro terminal HTML email.