Apache mod_php embeds PHP into every Apache worker process. PHP-FPM runs PHP in a separate pool that Apache proxies to. The difference in memory efficiency and concurrency handling is substantial — this page explains when the switch is worth making and how to do it.

What is the Difference Between Apache Handler and PHP-FPM?

With Apache Handler (mod_php), every Apache worker process loads a PHP interpreter into its own memory space. A server with 60 Apache workers has 60 PHP interpreters running simultaneously, regardless of whether any given request needs PHP. A request for a static image — a CSS file, a JS file, an image — still consumes a full PHP-loaded worker while it serves that file. This is the core inefficiency of mod_php: static content and PHP content compete for the same pool of heavy workers. PHP-FPM separates the concerns. Apache runs with the Event MPM, which handles static content using lightweight threads with minimal RAM per connection. PHP requests are proxied to a separate PHP-FPM process pool via mod_proxy_fcgi. The FPM pool is sized for PHP requests only — static requests never touch it. On a typical WordPress server serving 40% static assets, this means the Apache side can handle 2-3x more simultaneous connections with the same RAM. The memory per Apache Event worker is roughly 2-5MB versus 40-60MB per prefork/mod_php worker. PHP-FPM workers themselves still use 40-60MB each, but there are far fewer of them (sized only for peak PHP concurrency, not total connection count).

Tools and Commands

# Check current Apache MPM
apachectl -V | grep -i 'server mpm'

# Check which PHP handler is in use
apachectl -M | grep -i php
# mod_php active: shows 'php8_module'
# FPM active: shows 'proxy_fcgi_module'

# Check PHP-FPM status if running
systemctl status php8.1-fpm

# Measure Apache worker memory (mod_php scenario)
ps --no-headers -o rss -C apache2 | awk '{sum+=$1;n++} END{printf "Avg: %.1f MB  Total: %.0f MB  Count: %d\n", sum/n/1024, sum/1024, n}'

# Measure PHP-FPM worker memory (FPM scenario)
ps --no-headers -o rss -C php-fpm8.1 | awk '{sum+=$1;n++} END{printf "Avg: %.1f MB  Total: %.0f MB  Count: %d\n", sum/n/1024, sum/1024, n}'

On a server running mod_php, apachectl -M shows php8_module. On a PHP-FPM setup, it shows proxy_fcgi_module and proxy_module — there is no php module loaded into Apache. The memory comparison between the two commands confirms the difference in per-process footprint.

Key Parameters

Flag / ParameterDescriptionSecurity Note
MPM prefork (mod_php)Apache's multi-processing model required by mod_php. Each request is handled by a separate process with a full PHP interpreter embedded.Simplest to configure. Total memory = Apache workers × worker RSS (45-60MB). Does not scale to high static file concurrency without exhausting RAM. No process isolation between requests.
MPM event + PHP-FPMApache Event MPM handles connections asynchronously with threads; PHP is proxied to a separate FPM pool via mod_proxy_fcgi.Apache Event workers are 2-5MB each and handle hundreds of keep-alive connections per process. PHP-FPM pool is sized independently for PHP concurrency only. Total memory = (event_workers × 3MB) + (fpm_workers × 50MB). More efficient on mixed static+PHP workloads.
pm.max_children (FPM)Maximum number of PHP-FPM worker processes in the pool.Size as: (available_RAM_for_PHP) / (average_FPM_worker_RSS). On a 4GB server with 1GB for MySQL and 512MB for Apache Event, available for PHP: ~2000MB. At 50MB per worker: 40 FPM workers.
pm = dynamic vs ondemand (FPM)Process management mode: dynamic starts pm.start_servers workers immediately and scales between min/max; ondemand starts workers only when needed.Use dynamic for servers with consistent traffic — workers stay warm. Use ondemand for low-traffic or bursty servers — workers exit after pm.process_idle_timeout seconds. Ondemand has higher per-request latency for the first request after idle.
ProxyPassMatch (Apache)Apache directive that routes PHP file requests to PHP-FPM via FastCGI.Standard pattern: ProxyPassMatch ^/(.*\.php(/.*)?)$ unix:/run/php/php8.1-fpm.sock|fcgi://localhost/var/www/html. Always use Unix socket (not TCP port 9000) for local FPM — saves TCP overhead and avoids port conflicts.

Diagnosis and Fix Workflows

Decide whether to switch: measure the mod_php memory footprint

Before switching, quantify the current mod_php memory cost. If average worker RSS is above 40MB and the server is hitting MaxRequestWorkers limits or swapping, the switch to PHP-FPM will yield measurable improvement. If the server has plenty of headroom, the switch adds complexity without urgency.

# Measure current Apache+mod_php worker RSS
ps --no-headers -o rss -C apache2 | awk '{sum+=$1;n++} END{printf "Avg: %.1fMB  Total: %.0fMB  Workers: %d\n", sum/n/1024, sum/1024, n}'

# Check how often MaxRequestWorkers is being hit
curl -s http://localhost/server-status?auto | grep -E 'BusyWorkers|IdleWorkers'

# If BusyWorkers / (BusyWorkers + IdleWorkers) > 0.8 regularly, the pool is near saturation
# Check recent 503 errors
grep ' 503 ' /var/log/apache2/access.log | wc -l

Migrate from mod_php (prefork) to PHP-FPM (event MPM)

The migration involves three steps: install PHP-FPM, switch Apache to Event MPM with proxy_fcgi, and configure the FPM pool. This requires a maintenance window because Apache must be restarted with a different MPM.

# Step 1: Install PHP-FPM
apt install php8.1-fpm
systemctl enable php8.1-fpm
systemctl start php8.1-fpm

# Step 2: Switch Apache MPM from prefork to event
a2dismod php8.1
a2dismod mpm_prefork
a2enmod mpm_event
a2enmod proxy_fcgi setenvif
a2enconf php8.1-fpm

# Step 3: Configure FPM pool (/etc/php/8.1/fpm/pool.d/www.conf)
# pm = dynamic
# pm.max_children = 40
# pm.start_servers = 10
# pm.min_spare_servers = 5
# pm.max_spare_servers = 20

# Step 4: Test config and restart
apachectl configtest && php-fpm8.1 --test
systemctl restart apache2 && systemctl restart php8.1-fpm

# Step 5: Verify FPM is handling PHP
apachectl -M | grep proxy_fcgi

Verify PHP-FPM is actually serving requests (not Apache falling back)

After migration, confirm PHP requests are hitting FPM and not being served by any remaining mod_php configuration. Also confirm static files are handled directly by Apache Event without touching FPM.

# Verify FPM pool is receiving requests
php-fpm8.1 --test
cat /etc/php/8.1/fpm/pool.d/www.conf | grep 'pm.status_path'

# Enable FPM status page (add to pool.d/www.conf):
# pm.status_path = /fpm-status

# Check FPM status
curl http://localhost/fpm-status
# Should show: accepted conn, active processes, idle processes

# Confirm no php module in Apache
apachectl -M 2>/dev/null | grep -E 'php[0-9]'
# Should return nothing — php module must not be loaded alongside proxy_fcgi

Performance Impact: Static File Handling Determines Which Stack Wins

The choice between mod_php and PHP-FPM is almost entirely determined by the ratio of static file requests to PHP requests. A WordPress site without a caching plugin serves 100% PHP requests for page content — static assets are typically served from a CDN or separate static server. In this scenario, mod_php wastes memory but the concurrency advantage of PHP-FPM is minimal because there are no static requests to separate out. The switch still reduces per-worker memory by 3-5x (Apache Event workers are much lighter than prefork workers), which allows more total PHP capacity on the same RAM. On a server serving static assets from the same Apache instance — common on small shared hosting or servers without a CDN — the concurrency advantage is dramatic. A single 2GB RAM server with mod_php and 40 prefork workers provides 40 concurrent PHP requests maximum. The same server with PHP-FPM and Event MPM can handle hundreds of concurrent static file connections with Apache’s async threads while maintaining a FPM pool of 25-30 PHP workers. Traffic spikes that exhaust mod_php’s worker pool and generate 503 errors are absorbed by FPM’s queue (up to pm.max_children + FastCGI backlog) without immediate request failures.

  • Switching MPM from prefork to event requires disabling mod_php — the two cannot run simultaneously; Apache will refuse to start.
  • PHP-FPM Unix socket permissions must allow the Apache user (www-data) to connect — check socket file ownership after installation.
  • pm.max_children too large on a RAM-constrained server causes OOM exactly as mod_php's MaxRequestWorkers does — size FPM workers the same way.
  • ProxyPassMatch regex must match your PHP file patterns — test with a phpinfo() page first to confirm requests reach FPM before going live.

Practical Examples

Compare memory: mod_php prefork vs PHP-FPM event on the same server

# mod_php scenario (current state)
# Apache prefork workers: 40
# Average worker RSS: 52MB
# Total Apache memory: 40 × 52MB = 2080MB

# PHP-FPM + Event scenario (after migration)
# Apache Event workers: 12 (handle static + proxy)
# Average Apache Event worker RSS: 4MB
# PHP-FPM workers: 30 (PHP requests only)
# Average FPM worker RSS: 52MB
# Total: (12 × 4MB) + (30 × 52MB) = 48MB + 1560MB = 1608MB

# Savings: 2080MB - 1608MB = 472MB freed
# Real benefit: can now handle static files concurrently
# without consuming PHP workers

# Verify memory distribution after migration
ps --no-headers -o rss,comm | awk '/apache2/{a+=$1;ac++} /php-fpm/{f+=$1;fc++} END{printf "Apache: %.0fMB (%d procs)  FPM: %.0fMB (%d procs)\n",a/1024,ac,f/1024,fc}'

The numbers show 472MB freed on a 4GB server — enough for 9 additional FPM workers, which translates to higher PHP throughput under load.

Configure FPM pool for a 4GB server running WordPress

# /etc/php/8.1/fpm/pool.d/www.conf
[www]
user = www-data
group = www-data
listen = /run/php/php8.1-fpm.sock
listen.owner = www-data
listen.group = www-data

pm = dynamic
pm.max_children = 35
pm.start_servers = 8
pm.min_spare_servers = 5
pm.max_spare_servers = 15
pm.max_requests = 500

pm.status_path = /fpm-status
slowlog = /var/log/php8.1-fpm-slow.log
request_slowlog_timeout = 5s

; Verify with:
; systemctl reload php8.1-fpm
; curl http://localhost/fpm-status

pm.max_requests=500 recycles FPM workers after 500 requests — same principle as MaxConnectionsPerChild in Apache prefork. Prevents gradual RSS growth from PHP memory fragmentation.

Troubleshooting Common Issues

Problem: Apache returns 502 Bad Gateway after switching to PHP-FPM

Solution: FPM is not running or the socket path is wrong. Check: systemctl status php8.1-fpm and ls -la /run/php/php8.1-fpm.sock. The socket must exist and be owned by www-data. Verify the ProxyPassMatch path in Apache config exactly matches the socket file location.

Problem: PHP files are downloaded as text/plain instead of being executed after FPM migration

Solution: The ProxyPassMatch directive is not catching the PHP request. Check that mod_proxy and mod_proxy_fcgi are enabled (a2enmod proxy proxy_fcgi) and that the ProxyPassMatch pattern matches the PHP file URL. Also verify there is no remaining mod_php configuration in conf.d that is conflicting.

Problem: FPM pool exhausted — 502 errors during traffic peaks

Solution: pm.max_children is too low. Check FPM status: curl http://localhost/fpm-status — look at ‘listen queue’ (requests waiting for an available worker). Increase pm.max_children if RAM allows. If RAM is the constraint, consider enabling PHP OPcache to reduce per-request execution time and free workers faster.

Summary

Apache mod_php (prefork) embeds PHP in every worker — simple but memory-heavy, with total RAM scaling linearly with MaxRequestWorkers. PHP-FPM with Event MPM separates static and PHP processing — Apache Event handles static files with lightweight threads while a separate FPM pool handles PHP. The switch frees 20-30% memory on typical servers and eliminates the problem of static file requests consuming PHP-capable workers. Migration requires disabling mod_php, enabling Event MPM and proxy_fcgi, then configuring the FPM pool with pm.max_children sized the same way as MaxRequestWorkers.

  • Apache mod_php requires prefork MPM — every worker is 40-60MB with PHP embedded, regardless of whether the request needs PHP. PHP-FPM with Event MPM separates static and PHP handling, reducing Apache worker footprint to 3-5MB each.
  • Migrate with: a2dismod php8.1 mpm_prefork && a2enmod mpm_event proxy_fcgi — the two PHP handlers cannot coexist; Apache will not start with both loaded.
  • Size pm.max_children identically to MaxRequestWorkers: (available_RAM_for_PHP_MB) / (FPM_worker_RSS_MB) — measure actual FPM worker RSS with ps --no-headers -o rss -C php-fpm8.1.

Is Your Server Running at Full Performance?

INTRAM manages Linux servers with performance tuning built in from day one — correct MySQL configuration, PHP stack selection, nginx or Apache optimisation, and continuous monitoring so slowdowns are caught before users notice.

Explore Managed Hosting

Let’s assess what your business actually needs.

We will use these details only to understand your request and reply appropriately.