Apache performance tuning: MPMs, MaxRequestWorkers and memory

Updated November 2026

The single most important Apache setting is MaxRequestWorkers: how many requests the server works on at once. Set it too high and the machine runs out of RAM and starts swapping; the httpd documentation says a web server should never have to swap, because every request then slows to a crawl. Set it too low and visitors queue. This page shows how to pick an MPM, measure memory, and do the arithmetic, then covers the smaller settings and how to benchmark.

Pick the right MPM

An MPM (multi-processing module) decides how Apache uses processes and threads. Exactly one is loaded.

The three Unix MPMs in httpd 2.4
MPMHow it worksUse it when
eventSeveral processes, many threads each. A listener thread looks after idle keep-alive connections, so threads are only busy while a request is processed.Almost always. It is the default on Debian, Ubuntu, RHEL and Fedora.
workerLike event, but each open connection holds a thread, including idle keep-alive time.Rarely; event is the improved version.
preforkMany single-threaded processes, one connection each.Only for code that is not thread-safe, mainly mod_php.

Check which one is running:

apachectl -V | grep -i mpm
Server MPM:     event
# Debian/Ubuntu only:
a2query -M
event

If you still use mod_php, the best performance step is to move PHP to PHP-FPM and switch to event (see PHP with Apache). On Debian the PHP module's load file declares Depends: mpm_prefork, and installing libapache2-mod-php switches the server to prefork, so disable it first: sudo a2dismod php8.4 mpm_prefork && sudo a2enmod mpm_event, then restart.

How the limits fit together

MaxRequestWorkers
Requests served at the same time. Prefork: number of child processes. Event/worker: total number of threads. Extra connections wait in the kernel's listen queue.
ThreadsPerChild
Threads in each event/worker process. MaxRequestWorkers must be a whole multiple of it; Apache rounds down and logs a warning otherwise.
ServerLimit
Hard cap on processes. Event/worker need at least MaxRequestWorkers ÷ ThreadsPerChild; event uses extra slots for old processes that are finishing after a graceful restart.
ThreadLimit
Hard cap on ThreadsPerChild (default 64).
Built-in defaults (httpd 2.4 documentation)
Directiveevent / workerprefork
MaxRequestWorkers400 (16 × 25)256
ServerLimit16256
ThreadsPerChild25–
ThreadLimit64–
StartServers35
MaxConnectionsPerChild0 (never recycle)0

Debian and Ubuntu override these in /etc/apache2/mods-available/mpm_event.conf and mpm_prefork.conf, both with MaxRequestWorkers 150. On RHEL and Fedora the MPM is chosen in /etc/httpd/conf.modules.d/00-mpm.conf; put your sizing in a new file such as /etc/httpd/conf.d/mpm-tuning.conf.

Measure memory per process

Look at the server under normal load, not just after a restart, because processes grow as they handle requests:

ps -C apache2 -o rss= | awk '{n++; s+=$1} END {printf "%d processes, average %.0f MB\n", n, s/n/1024}'
# RHEL/Fedora: ps -C httpd ...
# PHP-FPM workers: ps -C php-fpm8.4 ... (Debian) or ps -C php-fpm ... (RHEL)

RSS (resident memory) counts shared libraries in every process, so the total is higher than real use. That makes the estimate conservative, which is what you want. Then decide how much RAM to keep for everything else on the machine: the operating system, the database, the page cache.

Worked example: prefork with mod_php

A server has 8 GB of RAM (8,192 MB). MariaDB and the system need about 2,048 MB. ps shows Apache children averaging 60 MB each.

<IfModule mpm_prefork_module>
    StartServers             5
    MinSpareServers          5
    MaxSpareServers         10
    MaxRequestWorkers      100
    MaxConnectionsPerChild   0
</IfModule>

100 is below prefork's default ServerLimit of 256, so ServerLimit is not needed. If you ever need more than 256, set ServerLimit to the same value as MaxRequestWorkers.

Worked example: event with PHP-FPM

Same 8 GB server, now with event and PHP-FPM. Apache threads mostly wait on PHP or on slow clients, so Apache itself is light; the PHP-FPM pool is what uses memory. Suppose ps shows Apache processes at about 30 MB each and PHP-FPM workers at about 50 MB each, and you want Apache to accept 400 simultaneous requests.

<IfModule mpm_event_module>
    StartServers             2
    ServerLimit             20
    ThreadLimit             64
    ThreadsPerChild         25
    MaxRequestWorkers      400
    MinSpareThreads         25
    MaxSpareThreads         75
    MaxConnectionsPerChild   0
</IfModule>

In the PHP-FPM pool file set pm.max_children = 110; that is the real limit on PHP requests at once. MaxSpareThreads must be at least MinSpareThreads + ThreadsPerChild (25 + 25 = 50), so 75 is fine.

A: prefork + mod_php, 8,192 MB System + DB 2,048 Apache: 100 × 60 MB = 6,000 MB spare 144 MB B: event + PHP-FPM, 8,192 MB System + DB 2,048 PHP-FPM: 110 × 50 MB = 5,500 MB Apache: 20 × 30 MB = 600 MB spare 44 MB
The same 8 GB server budgeted for each setup. With PHP-FPM, Apache's share is small and the PHP pool sets the limit.

Apply the change safely

ServerLimit and ThreadLimit only change on a full stop and start, so restart rather than reload:

sudo apachectl configtest
sudo systemctl restart apache2     # RHEL/Fedora: sudo systemctl restart httpd
# restart drops connections in progress; do it at a quiet time

Then watch the error log. These messages mean you really hit the limit:

AH00484: server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting   (event)
AH00286: ... (worker)    AH00161: ... (prefork)
AH03490: scoreboard is full, not at MaxRequestWorkers.Increase ServerLimit.   (event)

Raise MaxRequestWorkers only if the arithmetic still fits in RAM. If it does not, the fix is more memory, lighter processes (PHP-FPM, fewer modules), caching, or slow backends made faster. Watch live worker use with mod_status.

KeepAlive, Timeout and lookups

Unload modules you do not use

Each loaded module adds memory to every process. List them with apachectl -M. On Debian/Ubuntu remove one with sudo a2dismod name; on RHEL/Fedora comment out its LoadModule line in /etc/httpd/conf.modules.d/. Run apachectl configtest after each change: a module you thought was unused may be needed by a directive somewhere.

Benchmark before and after

Run load tests from a different machine (the tool uses CPU too), against a staging copy rather than production, and change one setting at a time.

# ApacheBench (apache2-utils on Debian, httpd-tools on RHEL)
ab -n 5000 -c 50 -k https://staging.example.com/

# wrk: 4 threads, 100 open connections, 30 seconds
wrk -t4 -c100 -d30s https://staging.example.com/

# h2load (nghttp2): HTTP/2, 100 clients, 10 streams each
h2load -n 10000 -c 100 -m 10 https://staging.example.com/

In ab's report, check Failed requests and Non-2xx responses first, then Requests per second and Time per request. For dynamic pages whose length varies, add -l so ab does not count them as failures. ab sends HTTP/1.0 requests, so use h2load to test HTTP/2. During a test, watch free -m on the server: any swapping means MaxRequestWorkers (or pm.max_children) is too high.

Mistakes to avoid

Sources