Apache Performance Tuning: Complete Guide
Advanced
Apache HTTP Server has been the backbone of the web since 1995, and despite Nginx overtaking it in market share, Apache remains the default choice for millions of websites — particularly those running PHP, WordPress, and shared hosting environments. Proper performance tuning transforms Apache from a resource-heavy default installation into a high-performance server capable of handling thousands of concurrent connections with modern MPM (Multi-Processing Module) configurations.
The key to Apache performance is choosing the right MPM and tuning it for your workload. The Event MPM, combined with PHP-FPM and proper module management, brings Apache's performance much closer to Nginx than many assume.
How Do Apache MPMs Affect Performance?
Apache's Multi-Processing Modules determine how the server handles concurrent connections. The choice of MPM is the single most impactful performance decision.
Prefork MPM creates a separate process for each connection. Each process handles one request at a time. This is the safest MPM (complete isolation between requests) but the most resource-intensive. Each process consumes 10-50MB of RAM, limiting concurrency. Prefork is required for non-thread-safe PHP extensions but should be avoided when possible.
Worker MPM uses multi-threaded processes. Each process spawns multiple threads, each handling one connection. Memory usage is much lower than Prefork because threads share process memory. However, all modules must be thread-safe.
Event MPM (recommended) extends Worker with an event-driven model for keepalive connections. Idle keepalive connections are handled by dedicated listener threads instead of blocking worker threads. This dramatically improves concurrency for workloads with many persistent connections — similar to Nginx's event-driven architecture.
How Do You Tune Event MPM Configuration?
# /etc/apache2/mods-enabled/mpm_event.conf
<IfModule mpm_event_module>
ServerLimit 16
StartServers 4
MinSpareThreads 75
MaxSpareThreads 250
ThreadsPerChild 25
MaxRequestWorkers 400
MaxConnectionsPerChild 10000
</IfModule>
MaxRequestWorkers (formerly MaxClients) is the most critical setting — it defines the maximum number of simultaneous connections. Set this based on available RAM: calculate available RAM for Apache, divide by per-worker memory usage (check with ps aux | grep apache). Going too high causes swapping; too low wastes capacity.
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
MaxConnectionsPerChild restarts worker processes after handling a set number of requests. This prevents memory leaks from accumulating. Set to 10,000-50,000 for applications with potential memory leaks; set to 0 (unlimited) for well-tested applications.
Why Does .htaccess Impact Performance?
Apache checks for .htaccess files in every directory in the path for every request. A request for /var/www/html/blog/2024/post.html triggers .htaccess checks in /var, /var/www, /var/www/html, /var/www/html/blog, and /var/www/html/blog/2024 — five directory lookups per request.
For high-performance sites, move all .htaccess rules into the virtual host configuration and disable .htaccess entirely:
<Directory /var/www/html>
AllowOverride None
</Directory>
This eliminates the per-request directory walks. On busy servers, this single change can improve throughput by 10-20%.
How Do You Configure Compression in Apache?
# mod_deflate configuration
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/plain text/css text/xml
AddOutputFilterByType DEFLATE text/javascript application/javascript
AddOutputFilterByType DEFLATE application/json application/xml
AddOutputFilterByType DEFLATE image/svg+xml
# Don't compress images, PDFs, or already-compressed files
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|webp)$ no-gzip dont-vary
SetEnvIfNoCase Request_URI \.(?:exe|t?gz|zip|bz2|sit|rar)$ no-gzip dont-vary
SetEnvIfNoCase Request_URI \.pdf$ no-gzip dont-vary
DeflateCompressionLevel 6
</IfModule>
# mod_expires for static asset caching
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
</IfModule>
How Should You Configure PHP with Apache?
The traditional mod_php loads PHP into every Apache worker process, consuming memory even for static file requests. PHP-FPM (FastCGI Process Manager) separates PHP processing from Apache, allowing Apache to handle static files efficiently while PHP requests are processed by a dedicated PHP process pool.
# Enable PHP-FPM with Apache Event MPM
<FilesMatch \.php$>
SetHandler "proxy:unix:/run/php/php-fpm.sock|fcgi://localhost"
</FilesMatch>
# PHP-FPM pool configuration (/etc/php/8.3/fpm/pool.d/www.conf)
[www]
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500
How Do You Optimize KeepAlive and Logging?
# KeepAlive optimization
KeepAlive On
MaxKeepAliveRequests 500 # Increase from default 100
KeepAliveTimeout 5 # Reduce from default 5 (or lower to 2 for high traffic)
# Log optimization
# Buffer log writes (reduces disk I/O)
BufferedLogs On
# Conditional logging: skip health check and static file requests
SetEnvIf Request_URI "^/health$" dontlog
SetEnvIf Request_URI "\.(css|js|ico|png|jpg|woff2)$" dontlog
CustomLog /var/log/apache2/access.log combined env=!dontlog
Reducing KeepAliveTimeout from the default 5 seconds frees up worker threads faster. On high-traffic sites, even 2 seconds is sufficient — most page resources load within 1-2 seconds. Increasing MaxKeepAliveRequests allows a single connection to serve more resources before closing, reducing TCP handshake overhead.