Technical deep dive
Apache DDoS Hardening: MPM Choice, mod_reqtimeout and Per-IP Limits
Last updated: August 2026 · MPM choice, mod_reqtimeout, mod_qos and mod_evasive · Reading time ~20 min

Apache's first DDoS decision is the MPM: prefork spends a process per connection and is exposed to Slowloris; event does not and is far more resilient. On top of the right MPM, mod_reqtimeout closes slow-request attacks, mod_qos limits connections per source, and mod_evasive blocks request floods. Each is a directive with a counter, sized against your own baseline.
This guide hardens Apache httpd itself against the connection- and request-exhausting part of a DDoS attack, with no device in front of it. Unlike nginx, Apache’s resilience is not a given — it is decided first by a choice most guides skip past: the Multi-Processing Module. Get that wrong and the module tuning below cannot save you; get it right and the modules do their job.
Every directive comes with its module, its value and the counter that shows it working. The values are starting points against your own traffic.
| Appliance | Attack shape | Apache mechanism | Verification |
|---|---|---|---|
| Slow-header / slow-body (Slowloris) | mod_reqtimeout RequestReadTimeout; event MPM | 408 rate in access log; scoreboard in server-status | |
| Many connections per source | mod_qos QS_SrvMaxConnPerIP | server-status busy workers; mod_qos console | |
| Request flood from few sources | mod_evasive DOSPageCount / DOSSiteCount | 403 rate; mod_evasive log/email hook | |
| Worker exhaustion | MaxRequestWorkers, ThreadsPerChild (event MPM) | server-status: busy vs idle workers | |
| Large-request abuse | LimitRequestBody, LimitRequestFields, LimitRequestLine | 413 / 400 rate in access log |
The first row decides the rest: on prefork, Slowloris exhausts processes before any other limit matters, so the MPM choice comes before the module tuning. None of it helps once the attack fills the circuit ahead of Apache.
0. Baseline first: turn on the scoreboard
Apache’s live state is the mod_status scoreboard. Enable it, restricted to localhost, and turn on extended status:
ExtendedStatus On
<Location "/server-status">
SetHandler server-status
Require ip 127.0.0.1
</Location>
# Worker scoreboard: counts per state
curl -s http://127.0.0.1/server-status?auto | grep -E 'BusyWorkers|IdleWorkers|Total Accesses'
# Per-client request counts from the access log, a normal week
awk '{print $1}' /var/log/apache2/access.log | sort | uniq -c | sort -rn | head
BusyWorkers against your MaxRequestWorkers ceiling, and the per-client rate from the log, are
the baselines every threshold below is measured against.
1. The MPM: the decision that comes before everything
This is the section that separates a resilient Apache from an exposed one. Check which MPM is active:
apachectl -V | grep -i 'Server MPM'
# or
apache2ctl -M | grep mpm
If it says prefork, that is the first thing to change unless a non-thread-safe module forces it.
prefork runs one process per connection, so a Slowloris attack exhausts the process pool with a few
thousand slow connections. event uses threads and a dedicated listener that offloads keep-alive
and lingering-close connections, so a slow connection costs a thread slot, not a process.
The usual reason a server is stuck on prefork is mod_php. Moving PHP to PHP-FPM frees you to run
event and is the single largest resilience gain here:
# mpm_event tuning — total capacity = ServerLimit * ThreadsPerChild
<IfModule mpm_event_module>
StartServers 4
ServerLimit 16
ThreadsPerChild 64
MaxRequestWorkers 1024 # ServerLimit * ThreadsPerChild
MaxConnectionsPerChild 10000 # recycle to bound memory growth
</IfModule>
# Confirm the running MPM and the ceiling
apachectl -V | grep -i mpm
curl -s http://127.0.0.1/server-status?auto | grep -E 'BusyWorkers|IdleWorkers'
MaxRequestWorkers is the hard ceiling on concurrent requests; set it to what your RAM supports,
not higher, because exceeding memory under load is its own outage.
2. mod_reqtimeout: the direct Slowloris answer
mod_reqtimeout caps how long a client may take to send its request. The stepped form gives a
generous initial window that shrinks as bytes arrive, distinguishing a slow-but-real client from a
one-byte-at-a-time attacker:
<IfModule mod_reqtimeout_module>
# header: 20s initial, extending 40s max at 500 bytes/s minimum
# body: 20s, extending at 500 bytes/s minimum
RequestReadTimeout header=20-40,MinRate=500 body=20,MinRate=500
</IfModule>
# Dropped slow requests appear as 408 in the access log
awk '$9==408' /var/log/apache2/access.log | wc -l
A rising 408 rate during an event is the timeout doing its job. If legitimate mobile clients start appearing in the 408s during normal traffic, the header window is too tight — widen the max and the MinRate together.
3. mod_qos: connections per source and overall
mod_qos is the connection-concurrency and fairness layer — the closest Apache equivalent to
nginx’s limit_conn:
<IfModule mod_qos_module>
QS_SrvMaxConn 2048 # total concurrent connections
QS_SrvMaxConnPerIP 50 # per source address
QS_SrvMaxConnClose 70% # disable keep-alive above this load, freeing slots
QS_SrvMinDataRate 150 1200 # min bytes/s, rising under load — a second Slowloris cut
</IfModule>
QS_SrvMaxConnClose is subtle and useful: above the given fraction of the pool, Apache stops
honouring keep-alive so connections free up faster. QS_SrvMinDataRate enforces a minimum
throughput that rises as the server fills, evicting the slowest connections exactly when slots are
scarce.
# mod_qos exposes its own counters
curl -s 'http://127.0.0.1/server-status?auto' | grep -i qos
4. mod_evasive: the request-flood block
mod_evasive counts requests per source over a short window and temporarily blocks a source that
crosses the threshold:
<IfModule mod_evasive20_module>
DOSHashTableSize 3097
DOSPageCount 5 # same page, per source, per interval
DOSSiteCount 50 # any page on the site, per source, per interval
DOSPageInterval 1 # seconds
DOSSiteInterval 1
DOSBlockingPeriod 30 # block duration in seconds
DOSLogDir /var/log/apache2/evasive
</IfModule>
# Blocked sources are logged and returned 403
awk '$9==403' /var/log/apache2/access.log | wc -l
ls /var/log/apache2/evasive/ # one lock file per currently-blocked source
Keep DOSBlockingPeriod short at first — a long block plus a false positive locks out a real user
for its full duration. As with every limit here, watch what it catches before you trust it.
5. Request size and field limits
Close the oversized-request surface, which can exhaust memory independent of connection count:
LimitRequestBody 10485760 # 10 MB
LimitRequestFields 100 # max header count
LimitRequestFieldSize 8190 # max header size
LimitRequestLine 8190 # max request-line length
Timeout 60 # global I/O timeout, lower than the default 300
KeepAliveTimeout 5 # short keep-alive idle
MaxKeepAliveRequests 100
# Oversized requests: 413 (body) and 400 (header/line)
awk '$9==413 || $9==400' /var/log/apache2/access.log | wc -l
The global Timeout default of 300 seconds is far too generous for a public server; lowering it is
a quiet but real Slowloris mitigation on its own.
6. Getting the client IP right behind a proxy
Every per-IP limit — mod_qos, mod_evasive — keys on the client address. Behind a proxy that is the
proxy’s address unless corrected with mod_remoteip:
<IfModule mod_remoteip_module>
RemoteIPHeader X-Forwarded-For
RemoteIPTrustedProxy 10.0.0.0/8 172.16.0.0/12
</IfModule>
# Log the real client, not the proxy
LogFormat "%a %l %u %t \"%r\" %>s %b" combined_realip
# If this returns 1, every request is attributed to the proxy and per-IP limits are meaningless
awk '{print $1}' /var/log/apache2/access.log | sort -u | wc -l
The signals to wire into monitoring
# 1. Busy workers vs ceiling — pool saturation
curl -s http://127.0.0.1/server-status?auto | awk -F': ' '/BusyWorkers/{print $2}'
# 2. 408 rate — slow-request timeouts firing
awk '$9==408' /var/log/apache2/access.log | wc -l
# 3. 403 rate — mod_evasive blocks
awk '$9==403' /var/log/apache2/access.log | wc -l
# 4. Scoreboard R-state wall — a Slowloris signature
curl -s http://127.0.0.1/server-status | grep -o 'R' | wc -l
A wall of workers in the R (reading) state is the unmistakable Slowloris signature; workers saturated in W (writing) is a request flood. The two look different on the scoreboard and call for different limits.
The honest limit of Apache hardening
Everything here defends against connection-state and request-rate exhaustion at the application edge. Two cases sit outside it.
The first is circuit saturation: volume that fills the pipe never reaches Apache, and no MPM or module changes that.
The second is the kernel layer beneath Apache — SYN queues, conntrack, descriptors — which a request passes before httpd sees it, and which the Linux hardening guide covers. Apache hardening assumes that layer is already in place. Above the circuit, the answer is the network tier; Apache keeps the application edge standing until then.
Order of application
- Enable mod_status; record a baseline week.
- Check the MPM. If prefork, move PHP to PHP-FPM and switch to event — before anything else.
- Set the event MPM capacity to what RAM supports.
- Add mod_reqtimeout; watch the 408 rate.
- Fix mod_remoteip if there is a proxy, before the per-IP limits.
- Add mod_qos and mod_evasive; keep block periods short until you trust them.
- Wire the four signals into monitoring.
Step 2 is not optional and not reorderable: on prefork, every limit below it is applied to a server that Slowloris can exhaust regardless.
Frequently asked questions
- Which MPM should I run for DDoS resilience?
- event, in almost every case. prefork runs one process per connection, so a few thousand slow connections exhaust the process pool — the classic Slowloris result — and prefork exists mainly for non-thread-safe modules like the old mod_php. event (and worker) use threads and a dedicated listener that hands off keep-alive and lingering connections, so a slow connection costs a slot, not a whole process. If you are on prefork only because of mod_php, moving PHP to PHP-FPM lets you switch to event and is the single biggest resilience gain available.
- Isn't mod_reqtimeout enough on its own against Slowloris?
- On event it is close; on prefork it helps but the MPM still limits you. RequestReadTimeout caps how long a client may take to send its request line and headers, so a connection dribbling headers is dropped at the timeout rather than held open. That is the direct Slowloris answer. But on prefork every held connection is still a whole process until the timeout fires, so the timeout has to be short and the process pool is still the ceiling. On event the same timeout is far more effective because the cost per stalled connection was low to begin with.
- mod_evasive or mod_qos — which do I need?
- They solve different shapes and many sites run both. mod_evasive counts requests per page and per site per source over a short window and temporarily blocks a source that crosses the threshold — a request-flood defence. mod_qos limits concurrent connections per source and overall, and can prioritise known-good traffic under load — a connection-concurrency and fairness defence. mod_evasive is quicker to deploy; mod_qos is more capable and more work to tune. Neither is a substitute for the right MPM and mod_reqtimeout underneath.
- What RequestReadTimeout values should I set?
- Start with a header timeout in the low tens of seconds that steps down as bytes arrive, and a shorter body timeout, then read the 408 rate. The stepped form — a longer initial allowance that shrinks as data comes in — distinguishes a slow but real client on a poor connection from a Slowloris client sending one byte at a time. Set it too aggressively and you cut mobile users on bad links; too loosely and slow attacks survive. The 408 rate against your baseline tells you which way to move.
- How do I observe Apache during an attack?
- mod_status with ExtendedStatus on. The scoreboard shows every worker's state — reading (R), sending (W), keep-alive (K), closing (C) — so a Slowloris attack shows as a wall of workers stuck in R, and a request flood as workers saturated in W. Restrict server-status to localhost, watch the busy-versus-idle worker count, and wire the busy count against MaxRequestWorkers into monitoring; when busy approaches the max, the pool is the bottleneck.
- Do these settings stop a volumetric attack?
- No. If the attack fills the circuit in front of the server, Apache never receives the requests and no MPM or module setting applies. Everything here defends against connection-state and request-rate exhaustion at the application edge. Volume above the circuit is a network-tier problem and is not solved in httpd.conf.
Published: August 2026
This guide is updated as vendors release new models and pricing. How we compare vendors