MPMs In Depth: Prefork, Worker & Event
Apache In Depth
Chapter 2 · MPMs In Depth: Prefork, Worker & Event
Web Servers Fundamentals Chapter 2 introduced Apache's three Multi-Processing Modules conceptually: Prefork (process-per-connection), Worker (processes plus threads), and Event (the modern default, which offloads idle keep-alive connections). This chapter tunes them for a real workload — the actual directives behind each one, how to check which is compiled in, the capacity math each one runs into, and the one dependency that has historically forced the choice for a huge number of real sites: PHP.
Checking Which MPM Is Actually Compiled In
Unlike Nginx, where the event loop is simply how the one binary works, Apache's MPM is chosen at build/package-install time and only one can be active at once. Most modern distributions ship mpm_event as the default package, but it's worth confirming rather than assuming — especially on an older server, or one where mod_php was installed at some point, since that package has historically pulled in mpm_prefork as a dependency.
Prefork In Depth
Prefork spawns a pool of single-threaded child processes in advance, each handling exactly one connection at a time. MaxRequestWorkers (the modern name for the older MaxClients) is the real concurrency ceiling: it directly caps the number of simultaneous connections, full stop, since there's no threading underneath to multiply it further. MaxConnectionsPerChild set to 0 means a child process is never recycled — convenient, but it means any slow memory leak inside a module a request touches accumulates for the lifetime of the server rather than being cleared periodically.
Worker In Depth
Worker runs a smaller pool of processes, each running multiple threads — ThreadsPerChild of them — so one process can hold several connections open at once, using less memory per connection than Prefork's one-process-each model. The real ceiling here is a genuine multiplication, the same shape as Nginx's own worker_processes × worker_connections math from Nginx In Depth Chapter 2: MaxRequestWorkers can go no higher than ServerLimit × ThreadsPerChild — in the defaults above, 16 × 25 = 400, which is exactly the value shown. Setting MaxRequestWorkers above that product without also raising ServerLimit simply gets silently capped at the lower number.
Event In Depth (the Modern Default)
Event shares Worker's exact directive set and the same ServerLimit × ThreadsPerChild math, plus one addition: AsyncRequestWorkerFactor. Worker's real weakness was that a thread holding an idle keep-alive connection open couldn't be reused for a new request — Event fixes exactly that, handing idle keep-alive connections off to a small dedicated listener thread instead of tying up a full worker thread. AsyncRequestWorkerFactor controls how many of those idle connections a single worker thread can effectively cover; the default of 2 means the real practical connection ceiling under Event can run meaningfully higher than MaxRequestWorkers alone would suggest for a workload with a lot of idle keep-alive time.
| Prefork | Worker | Event | |
|---|---|---|---|
| Real ceiling directive | MaxRequestWorkers directly | ServerLimit × ThreadsPerChild | Same product, plus AsyncRequestWorkerFactor headroom |
| Idle keep-alive cost | A full process each | A full thread each | Near-zero — handed to a listener thread |
| mod_php compatible | Yes — the traditional pairing | No — not thread-safe | No — not thread-safe |
mod_php and the Prefork Requirement
This is the one dependency that has forced more real-world MPM choices than any tuning preference: the traditional mod_php module runs the PHP interpreter directly inside each Apache worker, and that interpreter was never built to be thread-safe. Loading mod_php into a threaded MPM (Worker or Event) risks real crashes and corrupted output under concurrent load, so Apache refuses to load it under anything but Prefork. This single dependency is a large part of why so many long-running shared-hosting and WordPress deployments still run Prefork today, even on hardware that could otherwise easily handle Event's lower per-connection overhead.
The modern way around this is PHP-FPM (FastCGI Process Manager) — a separate, standalone PHP process pool that Apache talks to over a socket via mod_proxy_fcgi, rather than running PHP inside its own worker at all. Because PHP execution now happens in an entirely separate process, Apache itself is free to run Event (or Worker), getting Event's lower memory overhead under high concurrency on a PHP site — a genuine, concrete reason to migrate away from mod_php beyond just "it's older."
mod_status, enabled with ExtendedStatus On and a /server-status location, shows the real, live count of busy vs. idle workers or threads, broken down by what each one is currently doing (reading a request, sending a response, keep-alive, etc.). This is the practical way to tell whether the directives above are actually well-tuned for real traffic, rather than guessing from the config file's numbers alone.
worker_connections, covered in Nginx In Depth Chapter 2: each Prefork process or Worker/Event thread that's actually busy consumes real memory — under Prefork specifically, a full process's worth. Setting MaxRequestWorkers to a large number without checking whether MaxRequestWorkers × your application's real per-process memory footprint fits inside available RAM just means the server starts swapping heavily, or the OOM killer starts terminating processes, once that real ceiling is hit — regardless of what the config file says is allowed.
Hands-On Exercises
A server runs the Worker MPM with ServerLimit set to 16 and ThreadsPerChild set to 20, but MaxRequestWorkers is left at its old default of 400. Calculate the real effective concurrency ceiling this server will actually run into, and explain why it doesn't match the configured MaxRequestWorkers value.
📄 View solutionA team running Prefork sets MaxRequestWorkers to 2000 on a server with 4GB of RAM, where each PHP process typically uses 50MB, expecting this to significantly raise capacity. Using this chapter's own warning box, explain what will actually happen under real heavy load.
📄 View solutionExplain, in your own words, why a traditional mod_php installation forces Apache onto the Prefork MPM, and how switching to PHP-FPM changes that constraint.
📄 View solutionChapter 2 Quick Reference
- Prefork — one process per connection; real ceiling is
MaxRequestWorkersdirectly; the only MPM compatible with traditionalmod_php - Worker — processes + threads; real ceiling is
ServerLimit×ThreadsPerChild, notMaxRequestWorkersalone - Event — same math as Worker, plus
AsyncRequestWorkerFactorheadroom from offloading idle keep-alive connections to a listener thread - PHP-FPM (via
mod_proxy_fcgi) runs PHP in its own process pool, freeing Apache to run Event even on a PHP site mod_status//server-statusshows real, live busy/idle worker counts — the practical way to check tuning, not just the config file's numbers