Apache under attack: slowloris, floods, bad bots and AI crawlers

Updated November 2026

No setting inside Apache can absorb a large distributed attack. By the time the packets reach your server, the network link is already full. What Apache can do is drop slow and abusive clients quickly, so that one attacker or one aggressive crawler cannot use up all its workers. Floods are best stopped further upstream.

1 CDN / hosting provider: absorbs volume floods, rate rules 2 Firewall: per-IP connection limits, fail2ban bans 3 Front proxy (optional): limit_req per IP 4 Apache: mod_reqtimeout, worker limits, bot rules 5 Application: logins, search, expensive pages
The further out a request is dropped, the less it costs you. Apache is the fourth line, not the first.

Slow clients (slowloris)

A slowloris-style attack opens many connections and sends each request a few bytes at a time. Each connection holds a worker while Apache waits, so a single machine can exhaust a server that has a few hundred workers. The defense is mod_reqtimeout, which closes connections that send too slowly and answers them with 408 Request Timeout.

It ships with httpd 2.4 and is enabled by default on Debian and Ubuntu (mods-enabled/reqtimeout.conf). Check that it is loaded:

sudo apachectl -M | grep reqtimeout
# Debian, Ubuntu, if missing:
sudo a2enmod reqtimeout

The built-in default is handshake=0 header=20-40,MinRate=500 body=20,MinRate=500. In words: no limit on the TLS handshake. 20 seconds for the headers, extended by one second for every 500 bytes received, up to 40 seconds. 20 seconds for the body, extended the same way. On a site that is being attacked, tighten it:

RequestReadTimeout handshake=5 header=10-20,MinRate=500 body=20,MinRate=500

The handshake stage exists since 2.4.39. Do not set the body timeout so low that real uploads over slow mobile links fail. With MinRate=500, a 5 MB upload may take up to 20 + 5,000,000 ÷ 500 = 10,020 seconds before it is cut off, so slow but steady uploads still work. To see the timeouts in the log, add LogLevel reqtimeout:info.

Workers, timeouts and the event MPM

Watch the scoreboard in mod_status while you test. If almost every slot shows R (reading request), slow clients are the problem. See Server status.

Request floods from a few sources

A flood of complete, fast requests (often to an expensive URL such as search or a login page) is different. mod_reqtimeout does not help, because each request arrives quickly. The httpd documentation points to the firewall as the most effective first tool: limit simultaneous connections per IP address (for example with nftables or iptables connection-limit matches). Inside Apache, two third-party modules are commonly used.

mod_evasive

mod_evasive dates from the Apache 1.3 and 2.0 era. Today's actively packaged version is the fork at github.com/jvdmr/mod_evasive, which released version 2.4.0. Debian 13 packages it as libapache2-mod-evasive (2.4.0-2, as of November 2026). It counts requests per client IP and answers with 403, or another code you choose, once a limit is passed:

# Debian, Ubuntu: sudo apt install libapache2-mod-evasive
# then edit /etc/apache2/mods-available/evasive.conf
<IfModule mod_evasive.c>
    DOSEnabled          true
    DOSHashTableSize    3079
    DOSPageCount        5
    DOSPageInterval     1
    DOSSiteCount        100
    DOSSiteInterval     1
    DOSBlockingPeriod   60
    DOSHTTPStatus       429
    DOSWhitelist        127.0.0.1
</IfModule>

In the 2.x fork the module does nothing until you set DOSEnabled true. Debian's sample evasive.conf does not contain that line, so add it. The counters live in each child process, so the real limit per client is looser than the numbers suggest. Allow generous values for pages that load many images, and add your monitoring and office IPs to the whitelist. Many users behind one NAT or proxy share an IP and can trip the limits together.

mod_qos

mod_qos is a larger quality-of-service module. It can limit connections per IP, request rates per URL, minimum data rates and more. It is maintained at mod-qos.sourceforge.net (current release 11.79 as of November 2026). Debian 13 packages 11.76 as libapache2-mod-qos. A simple per-IP connection cap looks like this:

<IfModule mod_qos.c>
    QS_SrvMaxConnPerIP 50
</IfModule>

Its documentation recommends the worker MPM for connection-level controls, though event is used in practice. It also says to use only request-level controls with HTTP/2. Do not confuse it with httpd's own mod_ratelimit, which limits download bandwidth per response, not the number of requests.

fail2ban with Apache logs

fail2ban reads log files and bans IP addresses in the firewall after repeated matches. Version 1.1.0 is in Debian 13. Release 1.1.1 came out in August 2026. It ships Apache filters including apache-auth (failed logins), apache-badbots (known spam crawlers), apache-botsearch (probing for admin scripts), apache-noscript, apache-overflows, apache-shellshock, apache-fakegooglebot and apache-modsecurity. All jails are off until you enable them. Never edit jail.conf. Put your settings in /etc/fail2ban/jail.local:

[DEFAULT]
bantime  = 1h
findtime = 10m
ignoreip = 127.0.0.1/8 ::1 203.0.113.10

[apache-auth]
enabled = true

[apache-badbots]
enabled = true

[apache-botsearch]
enabled = true

[apache-modsecurity]
enabled = true

The log paths come from fail2ban's per-distribution files: /var/log/apache2/*error.log and *access.log on Debian, /var/log/httpd/*error_log and *access_log on Fedora and RHEL. Replace 203.0.113.10 with your own address. A custom filter for a WordPress login flood, saved as /etc/fail2ban/filter.d/apache-wplogin.conf:

[Definition]
failregex = ^<HOST> -[^"]*"POST /(?:wp-login\.php|xmlrpc\.php)
ignoreregex =
# add to jail.local
[apache-wplogin]
enabled  = true
port     = http,https
filter   = apache-wplogin
logpath  = /var/log/apache2/*access.log
maxretry = 10
findtime = 2m

Test the filter against a real log before enabling it, then reload and check:

sudo fail2ban-regex /var/log/apache2/access.log /etc/fail2ban/filter.d/apache-wplogin.conf
sudo systemctl reload fail2ban
sudo fail2ban-client status apache-wplogin

fail2ban-regex ends with a summary such as Lines: 4 lines, 0 ignored, 3 matched, 1 missed. If nothing matches, compare the regex with a real log line. Debian's other_vhosts_access.log starts with the virtual host name, so this filter skips those lines.

Behind a CDN or proxy, every connection comes from the proxy's addresses. Configure mod_remoteip (RemoteIPHeader and RemoteIPTrustedProxy) and log %a so the logs show real clients. A firewall ban on the client IP will still have no effect, though, because the packets come from the CDN. Ban at the CDN instead, or deny in Apache. See Reverse proxy.

Rate limits in front of Apache

If you run nginx or another proxy in front of Apache, rate limiting there is cheap, because rejected requests never reach Apache at all. nginx's limit_req module is the usual tool:

# nginx, in the http {} block
limit_req_zone $binary_remote_addr zone=perip:10m rate=5r/s;

server {
    location /search/ {
        limit_req zone=perip burst=10;
        proxy_pass http://127.0.0.1:8080;
    }
}

CDNs and hosting providers offer the same idea as rate-limiting rules, and they are the only place that can absorb a real volume attack. If you are attacked at a volume that fills your link, call your provider. Nothing you change on the server will help.

Bad bots and AI crawlers

robots.txt is a request, not a lock. RFC 9309, the robots.txt standard, says its rules "are not a form of access authorization". Well-behaved crawlers obey it. Bad ones ignore it or lie about who they are. Start with robots.txt for the crawlers that follow it:

# /robots.txt
User-agent: GPTBot
Disallow: /

User-agent: ClaudeBot
Disallow: /

User-agent: Google-Extended
Disallow: /

For bots that ignore robots.txt, refuse them in Apache. mod_rewrite works independently of your Require rules, so it cannot accidentally open anything up. The second condition still lets the bots read robots.txt:

RewriteEngine On
RewriteCond "%{HTTP_USER_AGENT}" "(BadBot|EvilScraper|ExampleScraper)" [NC]
RewriteCond "%{REQUEST_URI}" "!=/robots.txt"
RewriteRule "^" "-" [F]

Replace the example names with the user agents you actually see in your logs. Put the rules in the virtual host (or in .htaccess if you must), then test:

curl -s -o /dev/null -w '%{http_code}\n' -A 'Mozilla/5.0 (compatible; BadBot/1.0)' https://www.example.com/   # 403
curl -s -o /dev/null -w '%{http_code}\n' -A 'Mozilla/5.0 (compatible; BadBot/1.0)' https://www.example.com/robots.txt   # 200

User-agent rules only stop honest bad bots. A scraper that claims to be a normal browser passes straight through. For those, look at behavior in your logs (request rate per IP, odd URL patterns) with the methods in Log analysis, and use rate limits. Bots that claim to be Googlebot can be checked by reverse DNS. fail2ban's apache-fakegooglebot jail does this. The OWASP CRS project also offers a fake-bot plugin for ModSecurity (ModSecurity WAF).

Sources