There was a stretch a few years back where I was running several small client sites on a single 1GB RAM VPS, and Apache kept getting OOM-killed by the kernel during traffic bumps. That forced me to get serious about trimming Apache’s memory footprint rather than just throwing more RAM at the problem (which, frankly, isn’t always an option on a budget).
Why Apache’s Memory Usage Gets Out of Hand
Apache’s memory consumption is driven primarily by three things:
- The MPM in use (Prefork spawns a full process per connection — expensive; Event/Worker share memory across threads — cheaper).
- Loaded modules, many of which you might not actually need.
- The application layer behind Apache (like
mod_phpembedding the entire PHP interpreter into every Apache process).
Each of these compounds the others. A Prefork setup running mod_php is often the worst-case scenario for memory usage, since every single process carries a full copy of PHP in memory, whether or not that particular request needs it.
Prerequisites
- Sudo/root access to your server.
- Ability to restart Apache safely (ideally during low-traffic hours the first time).
- A baseline measurement of current memory usage, so you can prove the improvements to yourself afterward.
Step 1: Measure Your Current Memory Baseline
Before changing anything, establish where you stand:
ps -ylC apache2 --sort:rss | awk '{sum+=$8; count++} END {print "Average process size: " sum/count/1024 " MB, Total processes: " count}'
Also check overall system memory:
free -h
Write these numbers down. I always forget how satisfying it is to see the “after” numbers if I don’t record the “before” first.
Step 2: Switch to Event MPM (If You Haven’t Already)
If you’re still on Prefork MPM, switching to Event MPM is usually the single biggest memory win available, since threads inside a process share memory far more efficiently than fully separate processes.
sudo a2dismod mpm_prefork
sudo a2enmod mpm_event
If you’re using mod_php, note that it’s not compatible with threaded MPMs like Event or Worker. You’ll need to migrate to PHP-FPM first (see Step 3).
Step 3: Move from mod_php to PHP-FPM
This was the change that had the biggest impact for me personally. mod_php embeds the PHP interpreter directly into every single Apache worker process, ballooning memory usage even for requests that serve plain static files.
Install PHP-FPM:
sudo apt install php-fpm
Enable the required Apache modules:
sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.3-fpm
(Adjust the PHP version number to match what you have installed.)
Disable the old mod_php module if it’s active:
sudo a2dismod php8.3
Restart Apache:
sudo systemctl restart apache2
sudo systemctl restart php8.3-fpm
Now, Apache workers stay lightweight, and PHP execution happens in a separate, independently-managed pool of processes that only spin up as needed.
Step 4: Disable Unnecessary Modules
Every loaded module adds to Apache’s baseline memory footprint, even if it’s rarely exercised. List your currently enabled modules:
apache2ctl -M
Common modules I’ve been able to safely disable on many servers (only if you’re not using their functionality):
sudo a2dismod autoindex
sudo a2dismod status # only if you don't need /server-status
sudo a2dismod negotiation
sudo a2dismod cgid
Be careful here — don’t disable something like mod_rewrite or mod_ssl if you actually depend on it. Test after each change.
Step 5: Tune MaxRequestWorkers and ThreadsPerChild
Referring back to my Apache worker tuning post, set MaxRequestWorkers to match your actual available RAM rather than leaving Apache’s generous defaults in place. Lower concurrency ceilings directly cap how much memory Apache can consume at peak.
StartServers 2
MinSpareThreads 25
MaxSpareThreads 75
ThreadsPerChild 25
MaxRequestWorkers 100
MaxConnectionsPerChild 1000
Notice MaxConnectionsPerChild is set to a non-zero value here — recycling processes periodically helps prevent slow memory creep from any module with a memory leak.
Step 6: Enable Compression to Reduce Bandwidth-Related Memory Pressure
While this doesn’t directly cut Apache’s own memory footprint, enabling mod_deflate reduces the amount of data buffered and transmitted per request, which indirectly eases pressure on the system overall:
sudo a2enmod deflate
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css application/javascript application/json
Step 7: Set Sensible Timeouts
Long-lived idle connections tie up threads (and their associated memory) longer than necessary. Reduce your Timeout and KeepAliveTimeout values to something reasonable:
Timeout 60
KeepAliveTimeout 5
Real-World Use Cases
- Budget VPS hosting: Running several WordPress sites on a 1-2GB RAM droplet without constant OOM kills.
- Containerized deployments: Keeping Apache’s memory footprint predictable inside Docker containers with strict memory limits.
- Legacy application migration: Moving off
mod_phpas part of a broader modernization effort, gaining both memory savings and better PHP version isolation. - High-density shared hosting: Packing more customer sites onto the same physical hardware.
Troubleshooting Common Issues
Problem: Apache processes keep getting killed by the OOM killer. Check dmesg | grep -i oom to confirm. Lower MaxRequestWorkers, and consider adding swap space as a safety net (though swap should never substitute for proper tuning).
Problem: Memory usage climbs steadily over days, never releasing. This points to a memory leak, often in a third-party module or in your application code. Set MaxConnectionsPerChild to a reasonable non-zero number so processes recycle periodically and release accumulated memory.
Problem: Site broke after switching to PHP-FPM. Double check your .htaccess files don’t reference mod_php-specific directives like php_value or php_flag, since these aren’t recognized once mod_php is disabled. Move those settings into your pool’s php.ini or the FPM pool config instead.
Common Mistakes to Avoid
- Disabling modules blindly without checking what depends on them first.
- Assuming more RAM is always the answer instead of addressing root causes like
mod_phpor misconfigured MPM settings. - Setting
MaxConnectionsPerChildto0“for performance,” inadvertently allowing memory leaks to accumulate indefinitely. - Forgetting that a full restart (not reload) is required after MPM or PHP handler changes.
- Not measuring before and after, making it impossible to know if changes actually helped.
Security Best Practices
- Disabling unused modules also reduces your attack surface, not just memory usage — a nice two-for-one benefit.
- Keep PHP-FPM pools isolated per site (separate pool configs, separate users) so a compromise in one site doesn’t directly expose others.
- Regularly patch Apache and PHP-FPM, since memory-related bugs are sometimes tied to security vulnerabilities as well.
Performance Optimization Tips
- Pair memory reduction with caching (see my caching setup post) — fewer PHP executions means less peak memory demand.
- Use a lightweight reverse proxy cache (like
mod_cacheor an external Varnish layer) in front of Apache for static or semi-static content. - Monitor memory trends over time with tools like
htop,vmstat, or a proper monitoring stack (Netdata, Prometheus + Node Exporter) rather than checking manually and inconsistently.
Frequently Asked Questions
Does switching to Event MPM always reduce memory usage? In almost all cases, yes, especially compared to Prefork, since threads share memory more efficiently than fully separate processes. The exception is if you’re stuck using non-thread-safe modules that force you back to Prefork.
Is PHP-FPM harder to manage than mod_php? It has a slightly different configuration structure (pools, sockets), but most people find it not meaningfully harder — and the memory and flexibility benefits are substantial.
How much memory can I realistically save? In my own experience, moving from Prefork + mod_php to Event + PHP-FPM cut average memory usage by roughly 40-60% on typical WordPress-style workloads, though your results will vary based on your specific applications.
Will reducing MaxRequestWorkers cause dropped connections? Only if actual concurrent demand exceeds the new limit. Monitor /server-status after changes to confirm you haven’t set the ceiling too low for your real traffic.
Summary and Key Takeaways
Reducing Apache’s memory usage is rarely about one single tweak — it’s a combination of choosing the right MPM, moving heavy interpreters like PHP out of the main Apache process, disabling what you don’t need, and tuning concurrency limits to match your actual hardware.
The changes that gave me the biggest wins, in order of impact:
- Migrating from
mod_phpto PHP-FPM. - Switching from Prefork MPM to Event MPM.
- Tuning
MaxRequestWorkersandThreadsPerChildto match available RAM. - Disabling unused modules.
- Setting sane timeouts to free up resources faster.
Do these in order, measure after each step, and you’ll likely find — like I did — that your server can comfortably handle far more traffic on the same hardware than you thought possible.