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 / Parameter | Description | Security 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-FPM | Apache 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 -lMigrate 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_fcgiVerify 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_fcgiPerformance 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-statuspm.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_childrenidentically toMaxRequestWorkers:(available_RAM_for_PHP_MB) / (FPM_worker_RSS_MB)— measure actual FPM worker RSS withps --no-headers -o rss -C php-fpm8.1.
Related Commands
MaxRequestWorkers formula for mod_php prefork • OPcache configuration to reduce FPM worker execution time • PHP-FPM pm.max_children and pool configuration • diagnosing slow PHP requests with FPM slow log
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