Skip to main content

Apache: performance tuning and optimization

Hosting & cPanel · 27.02.2025 · 5 min read
Illustration for “Apache: performance tuning and optimization”

Choosing an MPM Module

The MPM (Multi-Processing Module) determines how Apache handles concurrent connections. The choice of MPM decides how much memory each request costs and how well the server plays with PHP-FPM:

MPMUse caseRecommendation
Preforkmod_php (legacy)Not recommended
WorkerMulti-threadedIf mod_php is not used
EventAsynchronousRecommended for PHP-FPM
# Switching to Event MPM
a2dismod mpm_prefork
a2enmod mpm_event
a2enmod proxy_fcgi setenvif
a2enconf php8.2-fpm
systemctl restart apache2

Prefork spins up a separate process for every connection and keeps it open until the request finishes — that is unavoidable with mod_php, but with PHP-FPM it no longer makes sense and wastes memory many times over.

Real Errors and Warnings in the Logs

The most common warning when workers run out under load:

[mpm_event:error] AH00485: scoreboard is full, not at MaxRequestWorkers
[mpm_event:error] server reached MaxRequestWorkers setting, consider
raising the MaxRequestWorkers setting

It means every available worker is busy handling requests, and new connections queue up or get dropped. The cause is usually not Apache itself but slow PHP scripts that hold a worker longer than they should — check slow.log first, and only raise the limit afterward. A second common error appears when the PHP-FPM connection is misconfigured:

[proxy_fcgi:error] AH01079: failed to make connection to backend:
httpd-UDS

It means Apache cannot reach the PHP-FPM socket — either the PHP-FPM daemon is not running, or the socket path in the proxy configuration does not match the real one.

Tuning KeepAlive

# /etc/apache2/apache2.conf
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5  # Lower from 15 to 5 seconds

A KeepAliveTimeout set too high keeps a worker occupied waiting for the next request from the same client, even after the client has closed the tab. On high-traffic sites this drains the worker pool for nothing.

Enabling Optimization Modules

a2enmod expires deflate headers
a2enmod http2  # HTTP/2 support

Logging Slow Requests

# In the VirtualHost config
CustomLog ${APACHE_LOG_DIR}/slow.log combined env=slow
SetEnvIf X-Request-Start t=([0-9]+) slow_start=$1

Enabling mod_status for Diagnostics

To see the number of busy and idle workers in real time, enable the module and restrict access to the status page to the local address only:

a2enmod status

# /etc/apache2/mods-enabled/status.conf
<Location "/server-status">
    SetHandler server-status
    Require local
</Location>

The Require local directive is critical: without it, the status page, which exposes internal URLs and client IP addresses, becomes reachable by any outside visitor.

Sizing MaxRequestWorkers by Memory

A MaxRequestWorkers value picked at random is a common cause of both the "scoreboard is full" error and the opposite problem, swapping from memory exhaustion. The right calculation starts from available RAM, not CPU core count:

  • Find the average memory footprint of one Apache process with ps aux | grep apache2 and the RSS column
  • Subtract the memory reserved for MySQL, PHP-FPM, and the system from the server's total RAM
  • Divide the remaining memory by the average footprint of a single process — that is the safe upper bound for MaxRequestWorkers

When Apache Tuning Will Not Help

  • The bottleneck is not the web server but the SQL queries themselves or an external API — MPM and KeepAlive settings cannot speed up something that is already waiting on a database response
  • PHP scripts hold a worker for seconds because of synchronous calls to third-party services — that needs a task queue or response caching, not an Apache tweak
  • The server has physically run out of CPU or memory — raising MaxRequestWorkers on exhausted hardware leads to swapping and a full hang, not more throughput
  • The site already sits behind Nginx as a reverse proxy in front of Apache — some optimizations (gzip, for example) are better configured at the Nginx level rather than duplicated in Apache

Verifying the Result

  1. Check which modules are actually active: apachectl -M | grep mpm should show event, not prefork
  2. Run a load test before and after the changes: ab -n 1000 -c 50 https://yourdomain.com/ and compare the average response time
  3. Watch mod_status (/server-status) for a few minutes under real load — the number of busy workers should not constantly hit the limit

Tuning PHP-FPM itself, including pool size and the number of child processes, is covered in the article PHP-FPM: pools, workers, and tuning. General .htaccess rules for speeding up static asset delivery are in the article .htaccess: redirect and optimization rules.

💡 Apache with Event MPM + PHP-FPM is comparable in performance to Nginx when configured correctly. Prefork is the main reason for Apache running slowly.
Was this article helpful?
← Back to Knowledge Base Ask Support