DevOpsil
Apache
91%
Needs Review

Apache Performance Tuning: MPM, KeepAlive, and Caching

Amara OkaforAmara Okafor7 min read

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

MPMModelBest ForPHP Support
preforkOne process per requestmod_php (non-thread-safe)mod_php only
workerThreads inside processesHigh concurrency, PHP-FPMPHP-FPM required
eventAsync keepalive + threadsHigh concurrency + KeepAlivePHP-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

ScenarioKeepAliveTimeoutMaxRequests
Reverse proxy (backend traffic)On2s100
Static file serverOn5s50
API-only serverOff or On1s10
High-concurrency, memory-limitedOn2s20

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:

OptimizationEffortExpected Gain
Switch to event MPMLowHigh — especially with KeepAlive
Reduce KeepAliveTimeoutTrivialMedium — frees threads faster
Enable mod_deflateLowHigh for text-heavy responses
Set browser cache headersLowHigh — reduces repeat requests
Enable mod_cache for static API responsesMediumVery high for cacheable endpoints
Tune MaxRequestWorkers to available RAMLowHigh — prevents OOM crashes
Enable sendfile()TrivialMedium 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
Share:

Was this article helpful?

Amara Okafor
Amara Okafor

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

More in Apache

View all →
ApacheQuick RefBeginnerNeeds Review

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.

Aareez Asif·
3 min read

Discussion