Apache Performance Tuning: MPM, KeepAlive, and Caching
A freshly installed Apache will handle modest traffic fine, but its default configuration is deliberately conservative — designed to work everywhere, not to be fast anywhere. This guide covers the three biggest levers for Apache performance: Multi-Processing Module (MPM) selection, KeepAlive tuning, and response caching.
Understanding MPMs
Apache's MPM determines how it spawns processes and threads to handle requests. Choosing the wrong one is the single biggest performance mistake you can make.
The Three MPMs
| MPM | Model | Best For | PHP Support |
|---|---|---|---|
prefork | One process per request | mod_php (non-thread-safe) | mod_php only |
worker | Threads inside processes | High concurrency, PHP-FPM | PHP-FPM required |
event | Async keepalive + threads | High concurrency + KeepAlive | PHP-FPM required |
event MPM is the right choice for most modern deployments. It handles KeepAlive connections asynchronously — a dedicated thread watches idle connections without tying up a worker thread, dramatically reducing memory usage under load.
Check Your Current MPM
apache2ctl -V | grep MPM
# output: Server MPM: event
# Or check loaded modules
apache2ctl -M | grep mpm
Switch to event MPM
# Disable prefork, enable event
sudo a2dismod mpm_prefork
sudo a2enmod mpm_event
# If using PHP, switch from mod_php to PHP-FPM
sudo a2dismod php8.1
sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.1-fpm
sudo systemctl restart apache2
MPM event Configuration
Once you're on event, tune it for your server's RAM and expected concurrency:
# /etc/apache2/mods-available/mpm_event.conf
<IfModule mpm_event_module>
# Number of server processes (set to CPU core count)
ServerLimit 4
# Processes to start at boot
StartServers 2
# Min/max spare threads across all processes
MinSpareThreads 25
MaxSpareThreads 75
# Threads per process
ThreadsPerChild 25
# Max simultaneous requests (ServerLimit * ThreadsPerChild)
MaxRequestWorkers 100
# Requests per child before it's recycled (prevents memory leaks)
MaxConnectionsPerChild 1000
# Async connections waiting for keepalive data
AsyncRequestWorkerFactor 2
</IfModule>
Sizing MaxRequestWorkers
The formula: MaxRequestWorkers = Available RAM / Memory per Apache process
# Check current Apache process memory usage
ps aux --sort=-%mem | grep apache2 | awk '{print $6}' | head -10
# Memory is in KB; calculate average
# Example: 1GB RAM available for Apache, ~30MB per process
# MaxRequestWorkers = 1024 / 30 = ~34 (be conservative, not aggressive)
For a server under heavy PHP-FPM load, Apache processes themselves are lean (they're just proxying to FPM). You can push MaxRequestWorkers higher — PHP-FPM has its own pool limits that act as the real bottleneck.
KeepAlive Tuning
KeepAlive lets a single TCP connection serve multiple HTTP requests. This is critical for performance with assets (CSS, JS, images) but can waste resources if set too loosely.
# /etc/apache2/apache2.conf or your VirtualHost
# Enable KeepAlive
KeepAlive On
# Max requests per connection
# Set higher for asset-heavy pages (15-100)
MaxKeepAliveRequests 100
# How long to wait for the next request (seconds)
# 5 seconds is sufficient for most use cases
# Don't set this too high or idle connections eat threads
KeepAliveTimeout 5
KeepAlive Decision Matrix
| Scenario | KeepAlive | Timeout | MaxRequests |
|---|---|---|---|
| Reverse proxy (backend traffic) | On | 2s | 100 |
| Static file server | On | 5s | 50 |
| API-only server | Off or On | 1s | 10 |
| High-concurrency, memory-limited | On | 2s | 20 |
For a reverse proxy setup where Apache sits in front of an app server, a shorter KeepAliveTimeout (1-2 seconds) is usually fine — the backend connection is fast and persistent.
Static File Serving Optimization
Before caching, make sure Apache is serving files efficiently:
# Enable sendfile() system call — much faster than read()+write()
EnableSendfile On
# Use the OS file cache
EnableMMAP On
# Compress text responses
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
AddOutputFilterByType DEFLATE text/javascript application/javascript
AddOutputFilterByType DEFLATE application/json application/xml
AddOutputFilterByType DEFLATE image/svg+xml
# Don't compress already-compressed formats
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|webp|gz|zip)$ no-gzip
</IfModule>
# Set far-future cache headers for versioned assets
<FilesMatch "\.(css|js|png|jpg|gif|ico|woff2)$">
Header set Cache-Control "max-age=31536000, public, immutable"
</FilesMatch>
mod_cache: Caching Dynamic Responses
mod_cache caches entire HTTP responses — extremely effective for pages or API responses that don't change per-user:
# Enable cache modules
sudo a2enmod cache cache_disk headers
# Cache configuration — place in global config or VirtualHost
<IfModule mod_cache.c>
# Disk-based cache
CacheEnable disk /
# Cache storage location
CacheDirLevels 2
CacheDirLength 1
# Cache sizes
CacheMaxFileSize 1000000 # Max 1MB per cached response
CacheMinFileSize 1 # Cache even tiny responses
CacheMaxExpire 86400 # Max TTL: 24 hours
CacheDefaultExpire 3600 # Default TTL: 1 hour
# Don't cache responses with Set-Cookie headers
# (prevents caching user-specific content)
CacheIgnoreHeaders Set-Cookie
# Honor no-cache and no-store headers from clients
CacheStoreNoStore Off
CacheStorePrivate Off
# Add cache status header for debugging
CacheHeader on
</IfModule>
# Set the cache root directory
CacheRoot /var/cache/apache2/mod_cache_disk
Create the cache directory:
sudo mkdir -p /var/cache/apache2/mod_cache_disk
sudo chown www-data:www-data /var/cache/apache2/mod_cache_disk
Verify caching is working by checking response headers:
curl -I https://example.com/api/static-data
# Look for: X-Cache: HIT or Age: 42
mod_expires for Browser Caching
Control how long browsers cache your assets:
<IfModule mod_expires.c>
ExpiresActive On
# Default expiry
ExpiresDefault "access plus 1 hour"
# HTML — short cache, pages change frequently
ExpiresByType text/html "access plus 5 minutes"
# CSS and JavaScript — longer, use versioned filenames
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
# Images
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/webp "access plus 1 month"
# Fonts
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType application/font-woff "access plus 1 year"
</IfModule>
Worker Process Monitoring
Use Apache's built-in status module to track performance in real time:
sudo a2enmod status
# Restrict to localhost
<Location "/server-status">
SetHandler server-status
Require local
</Location>
# Watch live stats
watch -n1 'curl -s http://localhost/server-status?auto'
# Key metrics to watch:
# BusyWorkers: currently handling requests
# IdleWorkers: waiting for requests
# ReqPerSec: requests per second throughput
# BytesPerSec: data throughput
Performance Testing Before and After
Always measure the impact of changes:
# ApacheBench — simple load test
ab -n 1000 -c 50 https://example.com/
# -n: total requests, -c: concurrent users
# More realistic test with wrk
wrk -t4 -c100 -d30s https://example.com/
# -t: threads, -c: connections, -d: duration
# Baseline metrics to capture:
# Requests per second
# Mean latency (ms)
# 99th percentile latency
# Error rate
Quick-Win Checklist
Work through these in order — each one typically provides measurable improvement:
| Optimization | Effort | Expected Gain |
|---|---|---|
| Switch to event MPM | Low | High — especially with KeepAlive |
| Reduce KeepAliveTimeout | Trivial | Medium — frees threads faster |
| Enable mod_deflate | Low | High for text-heavy responses |
| Set browser cache headers | Low | High — reduces repeat requests |
| Enable mod_cache for static API responses | Medium | Very high for cacheable endpoints |
| Tune MaxRequestWorkers to available RAM | Low | High — prevents OOM crashes |
| Enable sendfile() | Trivial | Medium for large static files |
After each change, reload Apache and run your load test again. The difference between an untuned default install and a properly configured one is typically 3-5x throughput improvement on the same hardware.
# Reload without dropping connections
sudo apache2ctl graceful
Was this article helpful?
DevSecOps Lead
Security-first mindset in everything I ship. From zero-trust architectures to supply chain security, I make sure your pipeline doesn't become your weakest link.
Related Articles
Apache 301 Redirect Loop: Diagnosing And Fixing Infinite Redirect Chains In VirtualHost And .htaccess Configurations
Few things are more frustrating than deploying what you think is a clean redirect configuration, only to have your browser throw an `ERR_TOO_MANY_REDIRECTS...
Apache 413 Request Entity Too Large: Fixing File Upload Failures With LimitRequestBody And PHP Configuration
The 413 error means Apache is rejecting your upload before it even hits PHP. Here's exactly what to change and where. Two layers can block large uploads: 1...
Apache 500 Internal Server Error: Diagnosing .htaccess Syntax Failures And Module Conflicts Step By Step
If you've been running web infrastructure long enough, you know the 500 Internal Server Error is Apache's way of saying "something's broken, and I'm not go...
Apache Mod_status Real-Time Server Monitoring With Grafana And Prometheus Integration
Monitoring Apache without proper observability tooling is like flying blind. You might catch a problem after your users start complaining, but by then, you...
Apache Mod_security WAF Rules For OWASP Top 10 Protection
If you're running Apache in production without a Web Application Firewall, you're essentially leaving your front door unlocked. mod_security is the industr...
Apache 403 Forbidden Error After Directory Permissions Fix: Troubleshooting SELinux And File Contexts
You've just spent an hour wrestling with Apache file permissions, carefully setting ownership to `apache:apache` and permissions to `755` on your directori...
More in Apache
View all →Fix Apache 'AH00558: Could not reliably determine server's FQDN'
Resolve the Apache AH00558 warning about not being able to determine the server's fully qualified domain name by setting ServerName correctly.
Fix Apache mod_rewrite Not Working
Diagnose and fix Apache mod_rewrite rules not being applied, covering module loading, AllowOverride settings, .htaccess issues, and rewrite logging.
Apache as a Reverse Proxy: mod_proxy Configuration Guide
Configure Apache as a reverse proxy using mod_proxy to route traffic to backend applications with load balancing and health checks.
Apache SSL/TLS Hardening: Get an A+ on SSL Labs
Harden Apache SSL/TLS configuration to achieve an A+ rating on Qualys SSL Labs with modern cipher suites and security headers.