Improving Apache performance starts with proving where requests spend time and whether worker capacity is being held by idle connections, uncached responses, compression work, TLS handshakes, or application backends. Tune one change at a time so a faster benchmark is not hiding new errors, swap pressure, or stalled upstream requests.
Apache throughput is shaped by the active MPM, keepalive behavior, response cacheability, compression filters, TLS session reuse, and how much dynamic work is pushed into PHP or upstream applications. Current Apache HTTP Server 2.4 guidance still points to the event MPM for idle-connection efficiency, mod_cache quick-handler caching for safe cacheable responses, mod_http2 on browser-facing TLS sites, and shared-memory TLS session caching for busy HTTPS hosts.
Commands use the Debian and Ubuntu packaging layout with apache2ctl, a2enmod, /etc/apache2, and the apache2 service name. Keep server-status restricted to local or trusted admin access, leave ExtendedStatus Off during normal operation, and restart instead of reload when changing MPM startup sizing directives.
$ ab -n 1000 -c 50 -k http://host.example.net/ This is ApacheBench, Version 2.3 <$Revision: 1903618 $> ##### snipped ##### Concurrency Level: 50 Time taken for tests: 0.842 seconds Complete requests: 1000 Failed requests: 0 Keep-Alive requests: 1000 Requests per second: 1187.59 [#/sec] (mean) Time per request: 42.103 [ms] (mean) ##### snipped #####
Keep the URL, Host header, cookies, and concurrency identical for every comparison. Benchmark a representative endpoint, not an admin page or a CDN-cached asset that hides Apache work.
$ sudo apache2ctl -V
Server version: Apache/2.4.66 (Ubuntu)
Server built: 2026-06-03T15:25:00
##### snipped #####
Server MPM: event
threaded: yes (fixed thread count)
forked: yes (variable process count)
##### snipped #####
Apache's event MPM lets idle keepalive sockets wait outside the main request worker path so worker threads can serve other requests. If the result is prefork, the highest-impact improvement is often moving PHP workloads to PHP-FPM and switching to event or worker.
Related: How to enable the event MPM in Apache
Related: How to configure PHP-FPM with Apache
$ curl -sS http://127.0.0.1/server-status?auto 127.0.0.1 ServerVersion: Apache/2.4.66 (Ubuntu) ServerMPM: event Server Built: 2026-06-03T15:25:00 ##### snipped ##### BusyWorkers: 1 IdleWorkers: 49 Scoreboard: _________________________W________________________....................................................................................................
Never leave server-status open to the public internet. Restrict it to Require local or a tightly controlled admin source list.
Apache's performance guide notes that ExtendedStatus On adds extra per-request timing calls. Leave it off for routine monitoring and enable it only during deeper troubleshooting.
Timeout 300 KeepAlive On MaxKeepAliveRequests 100 KeepAliveTimeout 5
KeepAliveTimeout 5 is a capacity tradeoff: higher values help slow or repeated clients avoid reconnects, while lower values return idle capacity sooner. Lower it only when idle keepalive connections are consuming capacity, and prefer the event MPM over disabling keepalive blindly.
Related: How to enable KeepAlive in Apache
StartServers 2 MinSpareThreads 25 MaxSpareThreads 75 ThreadsPerChild 25 MaxRequestWorkers 150 MaxConnectionsPerChild 0
On event and worker, MaxRequestWorkers is bounded by compatible ServerLimit and ThreadsPerChild values. Raising it too far can trigger swapping and make latency worse instead of better.
$ curl -sSI https://host.example.net/static/app.css HTTP/2 200 content-type: text/css ETag: "664f4d-19b02" Cache-Control: public, max-age=604800 ##### snipped #####
Apache's caching guide explains that mod_cache can serve hits from the quick handler before most request processing runs. That is a strong performance win for safe cacheable content, but it also means authenticated or per-user responses need deliberate exclusions.
$ curl -sS -H 'Accept-Encoding: br,gzip' -I https://host.example.net/ HTTP/2 200 content-type: text/html; charset=UTF-8 Vary: Accept-Encoding Content-Encoding: br
mod_brotli and mod_deflate are most effective on HTML, CSS, JS, JSON, and similar text payloads. Exclude already-compressed binaries such as JPEG, PNG, WebP, .gz, and video files so CPU is not wasted recompressing them.
Be careful compressing pages that reflect secrets or tokens over TLS. The current mod_brotli documentation still calls out the BREACH class of information disclosure attacks.
Related: How to enable Brotli compression in Apache
Related: How to enable gzip compression in Apache
Tool: HTTP Header Checker
<VirtualHost *:443> ServerName host.example.net Protocols h2 http/1.1 ... </VirtualHost>
Apache supports HTTP/2 in every shipped MPM, but the current upstream guide warns that prefork effectively processes one request at a time per connection. event is usually the better fit when the platform supports it.
If HTTP/3 is part of the plan, terminate QUIC on a CDN or frontend proxy and keep Apache as the origin behind that layer.
$ curl -sS --http2 -I https://host.example.net/ HTTP/2 200 content-type: text/html; charset=UTF-8 vary: Accept-Encoding ##### snipped #####
Use -k only on a staging or self-signed certificate. If the response still shows HTTP/1.1, verify that the request is reaching the updated 443 virtual host and that ssl plus http2 are loaded.
Related: How to enable HTTP/2 in Apache
$ sudo a2enmod ssl socache_shmcb Considering dependency setenvif for ssl: Module setenvif already enabled Considering dependency mime for ssl: Module mime already enabled Considering dependency socache_shmcb for ssl: Enabling module socache_shmcb. Enabling module ssl. To activate the new configuration, you need to run: systemctl restart apache2
<IfModule ssl_module>
SSLSessionCache shmcb:${APACHE_RUN_DIR}/ssl_scache(512000)
SSLSessionCacheTimeout 300
</IfModule>
Apache's current mod_ssl documentation still recommends the shared-memory shmcb cache for high-performance inter-process session reuse, and the default SSLSessionCacheTimeout remains 300 seconds.
PHP-FPM keeps PHP execution out of the Apache process model so the web tier can use the event MPM and keep idle connections from pinning one process per client.
Related: How to configure PHP-FPM with Apache
Related: How to enable the event MPM in Apache
$ sudo apache2ctl -M Loaded Modules: core_module (static) so_module (static) watchdog_module (static) http_module (static) log_config_module (static) unixd_module (static) version_module (static) ##### snipped #####
Keep HostnameLookups Off, prefer AllowOverride None where .htaccess overrides are not needed, and disable modules that do not serve the deployed workload. Each of those removes extra parsing or per-request work from the hot path.
$ sudo apache2ctl -t Syntax OK
Syntax checks catch broken includes and invalid directives before a reload turns them into an outage.
Related: How to test Apache configuration
$ sudo systemctl reload apache2 $ sudo systemctl restart apache2
MaxRequestWorkers, ThreadsPerChild, and related MPM sizing directives are startup-time settings. Use a full restart after those edits instead of assuming a reload applied them.
$ ab -n 1000 -c 50 -k http://host.example.net/ ##### snipped ##### Complete requests: 1000 Failed requests: 0 Requests per second: 1438.67 [#/sec] (mean) Time per request: 34.754 [ms] (mean) ##### snipped #####
A tuning change is ready to keep only when latency or throughput improves without new errors, swap activity, worker exhaustion, or backend resets. If one metric improves while logs show timeouts or failed upstream requests, roll back the last change and re-test.