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.

Steps to improve Apache performance:

  1. Capture a repeatable baseline before changing anything.
    $ 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.

  2. Check which multi-processing module (MPM) is active.
    $ 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.

  3. Use a local-only server-status endpoint to watch worker pressure and keep ExtendedStatus off unless timing detail is specifically needed.
    $ 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.

  4. Review keepalive and timeout settings before lowering them in small steps.
    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.

  5. Check the active MPM limits and size MaxRequestWorkers against real memory use plus observed queueing.
    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.

  6. Cache static responses aggressively, but keep personalized or protected content out of the fast cache path.
    $ 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.

  7. Compress only text-based responses and verify that Vary: Accept-Encoding is present.
    $ 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.

  8. Enable HTTP/2 on the TLS virtual host when the site serves many parallel assets to browsers.
    <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.

  9. Confirm that clients actually negotiate HTTP/2 after the change.
    $ 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.

  10. Enable shared-memory TLS session caching on busy HTTPS sites so reconnecting clients avoid a full handshake every time.
    $ 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.

  11. Move PHP sites to PHP-FPM when application code is the bottleneck or when mod_php is keeping the server on prefork.

    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.

  12. Remove request-time work that is not required.
    $ 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.

  13. Validate the full configuration after each edit.
    $ sudo apache2ctl -t
    Syntax OK

    Syntax checks catch broken includes and invalid directives before a reload turns them into an outage.

  14. Reload Apache for normal tuning changes, or restart it when MPM sizing directives changed.
    $ 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.

  15. Re-run the same benchmark and compare it with server-status plus the error log.
    $ 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.