skip to content

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%

answer

  1. the interpreter lives inside the child
  2. threads share one address space
  3. extensions, not just the core
  4. the app moves out, the MPM opens up
  5. two pools, sized separately

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.

solid answer

~50 s

`mod_php` embeds the PHP interpreter in the httpd child itself. A threaded MPM would run several requests concurrently in that one address space, and while PHP can be built thread-safe, the builds distributions ship generally are not, and third-party extensions are the bigger risk — one non-thread-safe extension is enough to make the whole arrangement unsafe. So the practical rule is mod_php means `mpm_prefork`, and prefork means one process per concurrent connection, each carrying a full interpreter, which is why memory per connection is measured in tens of MB. Worse, under prefork an idle keep-alive connection holds one of those PHP-laden processes. Moving to php-fpm splits the two: PHP runs in its own process pool, httpd talks to it over FastCGI, and httpd itself is free to run `mpm_event`. You then size the web tier and the PHP pool independently, static files no longer occupy a PHP process, and a PHP restart no longer means an httpd restart.

go deeper

for a junior

Know that mod_php runs PHP inside Apache while php-fpm runs it as a separate service, and that the in-process version forces the prefork MPM.

for a middle

Explain the mechanism: threaded MPMs run concurrent requests in one address space, and the interpreter plus its extensions are not safe there — so prefork, with its per-connection process cost, is the price.

for a senior

Show what the split buys operationally: independent sizing of the two pools, static requests that never touch an interpreter, and PHP restarts that do not disturb the web tier.

for a principal

Generalise the trade — embedding a runtime couples your server's concurrency model to that runtime's thread-safety — and use it when deciding whether any workload belongs in-process or behind a protocol.

## What mod_php does `mod_php` is the PHP interpreter loaded into httpd as a module. Every httpd child that could serve a PHP request carries the interpreter in its own address space, and a request is executed in-process — no socket, no serialization, no second daemon. That is genuinely fast per request and trivially easy to install, which is why the classic LAMP stack was built this way. The cost is that the interpreter's requirements become the server's requirements. ## Why threading is the problem Under `mpm_worker` or `mpm_event`, one child process runs many request threads in a single address space. Anything living in that space must tolerate concurrent execution: no unguarded global mutable state, no static buffers reused across calls. PHP can be built in a thread-safe configuration, but the packages distributions ship for use with a web server are generally the non-thread-safe build, and even where the core is safe, the long tail of third-party C extensions is where the guarantees fall apart. A single extension with a static buffer is enough. And the failure mode is not a clean refusal to start. It is intermittent corruption under concurrency: responses containing another request's data, sporadic segfaults that take down every thread in that child, bugs that vanish when you attach a debugger. This is why the guidance has always been categorical — if you load an in-process interpreter, run `mpm_prefork`, where each child is single-threaded and concurrency inside the address space never happens. ## What prefork then costs you Once you are pinned to prefork, the whole MPM cost model applies at its worst: - Concurrency equals process count. Serving 300 simultaneous connections means 300 processes. - Each process carries an interpreter, its loaded extensions, and its per-request memory, so resident size per child is typically an order of magnitude larger than a bare httpd child. Multiply by the concurrency ceiling and the ceiling that fits in RAM is far lower than people expect. - You lose the event MPM's handling of idle keep-alive connections. A browser holding a connection open between requests occupies a full PHP-carrying process for the whole `KeepAliveTimeout`. - Every connection is equally expensive regardless of what it asks for. A request for a small static image occupies exactly the same interpreter-laden process as a request that runs application code. Teams usually discover this as a memory ceiling: `MaxRequestWorkers` cannot be raised because the box would swap, so the site saturates at a concurrency far below what the CPU could handle. ## What php-fpm changes php-fpm runs PHP as a separate daemon with its own pool of worker processes, and httpd forwards PHP requests to it over FastCGI — mechanically, through `mod_proxy_fcgi`. The interpreter is no longer inside httpd, so: - httpd can run `mpm_event`. The web tier gets threaded workers and asynchronous handling of idle keep-alive connections; a connection sitting between requests no longer costs a PHP process, and a connection asking for a static file never touches PHP at all. - The two tiers are sized independently. The PHP pool has its own worker-count setting (`pm.max_children`), which is where you apply your memory arithmetic — that number times per-process PHP memory. The httpd side can be sized for connections rather than for interpreters, which are very different quantities. - Isolation improves. PHP can run as a different user than the web server, pools can be separated per site, and a PHP restart to pick up new code does not require restarting httpd or dropping connections. - You gain a queue with a defined boundary. When the PHP pool is full, requests wait for a PHP worker while httpd keeps serving everything else — instead of the entire web tier being the queue. The trade is real, not free: you now run and monitor two daemons, requests cross a socket, and you have a second saturation point to understand. But the shape is strictly better for anything that mixes static and dynamic content or runs meaningful concurrency, and it is why modern distributions and modern deployment guides default to the php-fpm arrangement rather than the in-process module. ## The interview point The question is really about a general principle with a famous instance: embedding a language runtime inside your web server couples the server's concurrency model to the runtime's thread-safety. When you decouple them, each tier gets to use the concurrency model that suits it. The same reasoning explains why application servers are generally reached over a protocol rather than loaded into the proxy in front of them.

  • PHP can be compiled thread-safe. Why is that not enough to run mod_php under mpm_event?
    Because the guarantee has to hold for everything loaded, not just the core. Third-party extensions are where unguarded global state survives, and the packages most distributions ship for web use are the non-thread-safe build anyway. The failure mode — intermittent cross-request corruption rather than a startup error — makes the gamble a poor one even when the core is safe.
  • Under php-fpm, where does the memory arithmetic that used to constrain MaxRequestWorkers now live?
    In the PHP pool's own worker-count setting, pm.max_children, multiplied by per-process PHP memory. httpd's worker count is then sized for connections rather than interpreters. Separating the two is the point: you stop paying interpreter-sized memory for connections that only fetch static files or sit idle in keep-alive.
  • What new failure mode do you take on by moving to php-fpm?
    A second saturation point. When the PHP pool is exhausted, requests queue for a PHP worker and httpd returns a gateway error rather than blocking the whole web tier — better containment, but a failure you must now monitor separately. You also operate two daemons and a socket between them instead of one process.

saying these in an interview costs you the question

  • Says mod_php is unsafe simply because it is old
  • Believes a thread-safe PHP build makes mpm_event automatically safe
  • Thinks php-fpm is slower because requests cross a socket
  • Sizes the PHP pool with the httpd worker count
  • Assumes static file requests also need an interpreter process

context