skip to content

MPMs & Performance vs Nginx

Apache can run one process per connection (prefork), threads inside processes (worker), or threads with an async keep-alive poller (event) — and the choice decides how much memory and concurrency you get. This is the classic Apache interview question because it forces you to explain, honestly, where a process/thread model loses to an event-driven server and where it still wins.

on this pageshow

questions

6

Apache httpd ships three main multi-processing modules — mpm_prefork, mpm_worker and mpm_event. How does each one map connections to processes and threads, and what specifically does mpm_event change?

level: middleimportance: must knowfreq 75%

answer

  1. one connection, one process
  2. threads shrink the per-connection cost
  3. what happens between two keep-alive requests
  4. a listener thread holds idle sockets
  5. still one thread per in-flight request

basics

~20 s

mpm_prefork dedicates one single-threaded process per connection; mpm_worker runs many threads inside a few processes; mpm_event is worker plus a listener thread that holds idle keep-alive connections so no worker thread is parked on them.

solid answer

~50 s

In Apache httpd the MPM owns how a connection becomes a unit of execution. `mpm_prefork` forks a pool of single-threaded children and gives each connection an entire process — maximum isolation, no thread-safety requirement, and the most memory per concurrent connection. `mpm_worker` is a hybrid: a few children, each running `ThreadsPerChild` threads, so a concurrent request costs a thread stack rather than a whole process, but every loaded module and library must be thread-safe. `mpm_event` keeps worker's threading and adds a listener thread in each child that holds connections sitting idle in keep-alive, so a worker thread is claimed only while a request is actually being processed and handed back as soon as the response is written. That is the whole point: under prefork and worker an idle persistent connection occupies a worker for the whole `KeepAliveTimeout`; under event it does not. Event does not make request handling asynchronous — a handler blocked on a database still holds its thread.

go deeper

for a junior

Be able to say that prefork gives each connection its own process while worker and event use threads, and that this is why prefork uses far more memory at the same concurrency.

for a middle

Explain the mechanics: children and threads per child, and precisely what the event MPM's listener thread does with a connection that is idle between two keep-alive requests.

for a senior

Show you can tell an idle-connection problem from a slow-request problem before recommending an MPM change, and state what each MPM demands of the modules you load.

for a principal

Own the trade as a platform choice: isolation and non-thread-safe module support versus memory per connection, and be clear about which class of production problem changing the MPM cannot solve.

## What an MPM is Apache httpd deliberately separates *how connections are accepted and dispatched* from *what happens to a request*. The multi-processing module owns the first half: the listening sockets, the pool of children, and which process or thread ends up running the request through the module chain. In httpd 2.4 the MPM is a loadable module and exactly one may be active; `httpd -V` (or `apachectl -V`) prints it as `Server MPM:`. ```apacheconf LoadModule mpm_event_module modules/mod_mpm_event.so ``` ## mpm_prefork: one process per connection The parent forks children ahead of demand (`StartServers`) and keeps a spare pool between `MinSpareServers` and `MaxSpareServers`, up to `ServerLimit` processes. Each child is single-threaded and serves exactly one connection at a time, so the number of simultaneously served connections equals the number of children, capped by `MaxRequestWorkers`. What you buy: hard isolation. A segfault in a handler kills one child and one connection. Nothing in the address space is shared with another request, so libraries that are not thread-safe are perfectly safe here. What you pay: a full process per concurrent connection. Without an embedded interpreter a child may be a few MB resident; with an in-process language module it can be tens of MB, and that number multiplies by your concurrency ceiling. ## mpm_worker: threads inside processes Worker runs a smaller set of children, each with `ThreadsPerChild` request-handling threads, so the theoretical concurrency is `ServerLimit × ThreadsPerChild` and the effective ceiling is `MaxRequestWorkers`. Spare capacity is now measured in threads (`MinSpareThreads`, `MaxSpareThreads`). A concurrent request costs a thread stack (`ThreadStackSize`) plus per-request pools rather than an entire process image, which is a large reduction in memory per connection. The costs are the mirror image of prefork's benefits. Every module, and every library those modules link, must be thread-safe. A crash takes down all threads in that child, not just one connection. Keeping several children rather than one is partly a blast-radius decision. ## The keep-alive problem, and what event fixes Under prefork and worker, the worker that accepts a connection stays bound to it for the connection's whole lifetime — including the idle gaps between requests on a persistent connection. With `KeepAliveTimeout 5`, a browser that sends one request and then thinks for five seconds keeps that worker doing nothing for five seconds, while it still counts against `MaxRequestWorkers`. On a keep-alive-heavy site, most of your workers can be idle-but-occupied. The historical workaround was to set `KeepAliveTimeout` very low, which throws away connection reuse. `mpm_event` keeps worker's process/thread structure and adds a dedicated listener thread per child. Connections that are idle in keep-alive (and connections in lingering close) are handed to that listener, which polls them; a worker thread is claimed only when a request is actually readable and is released as soon as the response is written. So concurrency in *connections* decouples from concurrency in *requests being processed*, and you can keep a generous `KeepAliveTimeout` without paying a worker for it. The number of connections a child will hold beyond its busy workers is not unlimited — `AsyncRequestWorkerFactor` bounds it. ## What event does not change Request handling is still synchronous and still one thread per in-flight request. If your handler blocks for 400 ms on a database call, that thread is unavailable for 400 ms exactly as it would be under worker. Event fixes *idle connections*, not *slow requests*, and it is not an event loop in the sense that an asynchronous server is — the async part is confined to connection management. Because event inherits worker's threading, it inherits the thread-safety requirement too. That is precisely why an in-process language module built without thread safety pins a deployment to prefork and denies it both worker's memory savings and event's keep-alive handling. ## Choosing between them Event is the default for httpd 2.4 builds and the right answer for ordinary HTTP workloads: static content, reverse proxying, and any application reached over a protocol rather than embedded in the server. Worker is mainly of historical interest now — event is worker plus a strict improvement for keep-alive, with the same requirements. Prefork survives for exactly one reason: a module in your stack is not safe to run in a threaded child, and you would rather pay memory than debug data races in someone else's C code. In an interview, the sentence that shows you understand the family is: prefork trades memory for isolation, worker trades isolation for memory, and event trades nothing — it removes the cost of *waiting* connections while leaving the cost of *working* threads exactly where it was.

  • If mpm_event is strictly better than mpm_worker, why does mpm_worker still ship?
    Mostly history and conservatism. Event is worker plus asynchronous handling of idle keep-alive connections, and it carries the same thread-safety requirements, so there is rarely a technical reason to choose worker on a modern build. Worker remains for platforms or module stacks where the event MPM's polling behaviour has caused trouble, and as a fallback that is easy to reason about.
  • Does switching to mpm_event help a server whose handlers spend most of their time waiting on a slow backend?
    Barely. Event frees workers from idle keep-alive connections, not from in-flight requests. A thread blocked on a slow upstream is occupied under every MPM. You would see a benefit only if a large share of your worker occupancy was connections waiting between requests — check whether workers are in keep-alive state or actually processing before expecting a win.
  • Why does the choice of MPM affect which modules you can load?
    Prefork children are single-threaded, so a module or library with global mutable state is safe there. Worker and event run many threads in one address space, so every loaded module must be thread-safe. Loading a non-thread-safe module under a threaded MPM produces intermittent corruption and crashes rather than a clean startup error, which is why prefork persists.

saying these in an interview costs you the question

  • Says mpm_event makes Apache fully asynchronous like an event-loop server
  • Claims prefork is faster because processes are cheaper than threads
  • Thinks event MPM frees a worker blocked on a slow database call
  • Believes worker and event have no thread-safety requirements
  • Says the MPM is chosen at compile time in httpd 2.4

context

open as a page

In Apache httpd running mpm_event, how do MaxRequestWorkers, ServerLimit and ThreadsPerChild relate, and what happens if you set MaxRequestWorkers higher than ServerLimit multiplied by ThreadsPerChild?

level: middleimportance: must knowfreq 60%

basics

~20 s

MaxRequestWorkers is the total worker threads across all children, and it cannot exceed ServerLimit times ThreadsPerChild. Set it higher and httpd logs a startup warning and silently lowers it to that product, so your intended concurrency never takes effect.

open as a page

Why does serving PHP through the in-process Apache module mod_php effectively pin the server to mpm_prefork, and what changes when the same application moves to php-fpm?

level: middleimportance: should knowfreq 50%

basics

~20 s

mod_php runs the interpreter inside each httpd child, and the interpreter build plus its extensions are generally not thread-safe, so only single-threaded prefork children are safe. php-fpm moves PHP into its own process pool, freeing httpd to run mpm_event.

open as a page

An Apache httpd server stops keeping up: requests queue for seconds and the error log repeats 'server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting', while the machine's CPU is mostly idle. How do you work out whether raising it is actually the right fix?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Idle CPU with every worker busy means workers are blocked waiting, not computing. Find what they wait on — a slow backend, or idle keep-alive connections under prefork or worker — before raising the ceiling, because raising it multiplies memory use and downstream load.

open as a page

Your team runs Apache httpd with mpm_event in front of an application, and someone proposes replacing it with an event-driven server such as nginx 'for performance'. How would you decide, and where does a process-and-thread server genuinely lose to an event loop?

level: principalimportance: should knowfreq 45%

basics

~20 s

Decide from the traffic's shape, not the reputation. A thread-per-request server loses when concurrent connections vastly outnumber active requests — each needs a thread and its stack, versus a few KB of state in an event loop. If mpm_event already keeps up, the bottleneck is usually the application.

open as a page

How do you determine which multi-processing module a running Apache httpd 2.4 instance is using, and how would you switch it to a different one?

level: juniorimportance: nice to knowfreq 38%

basics

~20 s

Run httpd -V (apache2ctl -V on Debian) and read the Server MPM line, or list loaded modules with httpd -M. Switching means loading a different mpm_* module — only one may be active — and doing a full restart.

open as a page