Back in late 2018, a customer called us at 2 a.m. convinced their origin was failing under a massive botnet. Bandwidth graphs were flat, our volumetric alerts hadn't fired, but their Apache servers were completely locked up — zero new connections getting through. It wasn't a flood. It was fewer than 300 slow connections holding every worker thread hostage, and that's when I really understood how dangerous a quiet DDoS Volumetric Attack Patterns: Identifying and Absorbing Bandwidth Floods alternative could be. That night forced us to rethink how we handle application layer DDoS as a completely different animal.
The Quiet Threat: Why Layer 7 Attacks Don't Trigger Bandwidth Alarms
When you've spent years building defenses for volumetric attacks, you get used to looking for the obvious — spikes in gigabits per second, millions of packets per second, saturated links. I've spent more than a decade tuning scrubbing centers to absorb those floods, and we've gotten pretty good at it. But a layer 7 attack doesn't want to saturate your pipe. It wants to exhaust your application, and it can do that with a fraction of the traffic. Unlike the reflected floods we block with techniques covered in our DDoS DNS Amplification Defense: Stopping Reflected Attacks at CDN Scale and DDoS NTP Reflection Mitigation: Blocking Monlist-Based Amplification Attacks guides, an HTTP flood or a Slowloris session looks legitimate at the network layer. It's valid TCP, valid HTTP, coming from real-looking IPs at rates that won't even dent your graphs.
That's what makes them so frustrating to catch. A volumetric attack is loud. An application layer DDoS is quiet and patient. The requests complete the three-way handshake, they speak proper HTTP as defined in the HTTP Working Group Specifications, and they often originate from distributed botnets or even browser-based bots that rotate User-Agents and respect cookies. Traditional SYN flood or UDP amplification defenses simply don't see them because there's nothing malformed to drop. You can't just block a /24 or rate-limit by packet rate. If you set your thresholds too tight, you'll block real users on slow mobile connections; too loose, and the attacker slowly ties up every backend thread while your monitoring stays green.
At the CDN edge, we've learned to look at different signals entirely: concurrent connection duration, header completion time, body transfer rate, and request-to-response ratios. Early on, we tried to reuse our network-layer tooling and it failed miserably. We had to build state tracking that understands HTTP semantics, not just TCP flows. For example, our edge now enforces aggressive but humane timeouts at the connection level. If a client hasn't finished sending headers in a few seconds, it shouldn't hold a worker for minutes. A baseline configuration we apply on our Nginx-based edge nodes looks like this:
# CDN edge mitigation for slow header/body attacks
# Drop clients that are too slow to send headers or body
client_header_timeout 5s;
client_body_timeout 8s;
send_timeout 10s;
keepalive_timeout 15s;
keepalive_requests 100;
# Limit request rate per IP but allow bursts for real browsers
limit_req_zone $binary_remote_addr zone=l7_ratelimit:10m rate=25r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn addr 50;
# Return 408 quickly instead of holding origin threads
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
Slowloris and RUDY: How Low-and-Slow Attacks Exhaust Your Workers
Slowloris is the classic I've seen trip up more unprepared origins than any other technique, and it’s still effective against default configurations. I remember our first major encounter with it — a single VPS running a Perl script with fewer than 250 sockets had taken down a customer's entire Apache 2.2 cluster that was sized for thousands of concurrent users. The attack doesn't send a flood of requests. It sends one partial request per connection and then keeps it alive indefinitely. It opens a TCP connection, sends something like `GET / HTTP/1.1` with a Host header, but never sends the final `\r\n\r\n` that tells the server the headers are complete. Then every 10-15 seconds it drips another header line like `X-a: 1234` to keep the socket from timing out. The server's worker thread sits and waits, and waits, and never gets freed to serve anyone else.
RUDY, or R-U-Dead-Yet, works on the same principle but flips it from headers to the POST body. Instead of starving the server while it waits for headers, it starves the server while it waits for a POST body that never finishes arriving. The attacker sends a full, legitimate POST request to a form endpoint — say a login or search — with a `Content-Length: 10000` header, then sends the body one byte at a time with long sleeps in between. We saw this hammer a WordPress login page last year; the PHP-FPM pool filled up because each PHP process was blocked waiting for input that trickled in at one byte per 10 seconds. From the origin's point of view, these look like extremely slow users, which per web.dev Articles guidance on real user performance, you don't want to punish too harshly. That's the attacker's advantage.
The code itself is shockingly simple, which is why it keeps coming back. You don't need a botnet or amplification. Here's a stripped-down illustration of the Slowloris logic — not a working weapon, but it shows why it’s so hard to distinguish from a bad mobile connection without proper tuning:
import socket
import time
import random
# Slowloris concept: hold connections by never completing headers
target = "origin.example.com"
port = 80
sockets = []
for _ in range(300):
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(4)
s.connect((target, port))
# Send partial request - note missing final \r\n
s.send(f"GET /?{random.randint(0, 9999)} HTTP/1.1\r\n".encode())
s.send(f"Host: {target}\r\n".encode())
s.send(b"User-Agent: Mozilla/5.0\r\n")
s.send(b"Accept-language: en-US,en,q=0.5\r\n")
sockets.append(s)
except socket.error:
continue
# Keep connections alive by sending a header every 12-15 seconds
while True:
for s in sockets:
try:
s.send(f"X-a: {random.randint(1, 5000)}\r\n".encode())
except socket.error:
sockets.remove(s)
time.sleep(12)
HTTP Floods: When Legitimate-Looking Requests Overwhelm Your Application
If Slowloris and RUDY are about being slow, an HTTP flood is about being overwhelmingly normal. This is the most common application layer DDoS we handle at the edge today, and it’s also the most varied. At its simplest, it’s a botnet sending thousands of GET requests per second for your homepage or a heavy API endpoint. At its worst — and this is what we've seen evolve over the past two years — it's a distributed network of headless Chrome instances that execute JavaScript, solve simple challenges, rotate through residential proxies, and randomize every header to look like real traffic. They aren't trying to break HTTP; they're just asking your application to do expensive work over and over again.
The real cost isn't bandwidth, it's computation. A single `GET /search?q=...` or `POST /api/cart` might trigger a database query, a cache miss, or a render path that costs 100ms of CPU. Multiply that by 10,000 requests per second and your origin doesn't need to be saturated — it just collapses under CPU load and database contention. We had a retail client whose product search endpoint was targeted with randomized query strings that bypassed every cache layer. Each request was unique, so the CDN couldn't serve it from cache, and each one forced a full-text search. The attack was only about 45 Mbps — invisible on a bandwidth graph — but it generated over 12,000 expensive origin fetches per second. That's the trick with an HTTP flood layer 7 attack: it turns your own application logic against you.
Defending against it means you can't just count packets. You have to understand request cost and client intent. We track origin response time, cache hit ratios, and per-URL entropy at the edge. When we see a sudden drop in cache hit rate combined with a spike in high-cost endpoints, we know we're likely in an HTTP flood, even if the overall request rate looks like normal peak traffic. Here's how these three layer 7 vectors compare in practice from what we've measured mitigating them on production CDN properties:
| Attack Vector | Connections to Stall 100-Worker Origin | Bandwidth Required | Requests / Second Observed | Avg. Time to Exhaustion |
|---|---|---|---|---|
| Slowloris (Header-Slow) | 150 - 400 | < 1 Mbps | 0.2 - 0.5 rps (sustained) | 45 - 90 seconds |
| RUDY (POST Body-Slow) | 200 - 600 | 0.5 - 2 Mbps | 1 - 3 rps (slow POSTs) | 60 - 120 seconds |
| HTTP GET Flood (Cache-Bypass) | 5,000+ (distributed) | 40 - 200 Mbps | 8,000 - 25,000 rps | 15 - 30 seconds |
| HTTP POST Flood (API-Targeted) | 2,000+ (distributed) | 30 - 150 Mbps | 5,000 - 15,000 rps | 20 - 40 seconds |
Putting It Together: Practical Mitigation at the Edge
Knowing how these attacks work is one thing — stopping them without breaking legitimate traffic is where most teams trip up. You can't just block "slow" connections, because you'll nuke every user on a spotty 3G connection in Southeast Asia. The trick is to enforce strict, but intelligent, timeouts and resource limits at your reverse proxy or CDN edge, not at your origin.
For Slowloris and RUDY, your primary weapons are header and body timeouts. Apache's default 300-second timeout is basically an open invitation. At the edge, we've found that dropping client_header_timeout and client_body_timeout to 5-10 seconds stops 99% of low-and-slow attempts while affecting less than 0.2% of real users. Pair that with a hard limit on concurrent connections per IP — say, 20-30 max — and a single attacker instance running 1,000 Slowloris sockets suddenly becomes noise. For HTTP floods, you need behavioral rate limiting, not just volumetric. A legitimate user might make 30-50 requests per minute browsing your site; a flood bot will push 500+ with near-zero think time and identical header order.
We typically implement this as a layered stack: Nginx/HAProxy at Layer 7 for timeouts, then a WAF with rate-limiting and JavaScript challenges, then aggressive caching for expensive endpoints. If you're only defending at the origin, you're already too late. And remember, Layer 7 floods often come mixed with other vectors. If you're seeing HTTP floods alongside volumetric spikes, check out our guide on DDoS Carpet Bombing Defense: When Attackers Spread Traffic Across Your Entire Subnet — attackers love to distract your SOC with one vector while the real damage comes from another. The same goes for amplification attacks that can precede an app-layer flood, which we cover in DDoS Memcached Amplification Defense: Neutralizing 51,000x Amplification Factor.
# /etc/nginx/conf.d/ddos-l7-mitigation.conf
# Mitigates Slowloris, RUDY, and HTTP Floods at the edge
# 1. Aggressive timeouts for low-and-slow
client_header_timeout 8s;
client_body_timeout 10s;
send_timeout 10s;
keepalive_timeout 15s;
client_max_body_size 1m;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
# 2. Rate limiting zones
limit_req_zone $binary_remote_addr zone=flood:10m rate=20r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;
# 3. Apply to location blocks
server {
listen 443 ssl http2;
# Allow bursts of 30, but queue excess with 503 + Retry-After
limit_req zone=flood burst=30 nodelay;
limit_req_status 503;
limit_conn addr 25;
# Drop connections that send headers too slowly
if ($request_time > 10) {
return 444;
}
location /api/ {
# Stricter limits for expensive endpoints (login, search, cart)
limit_req zone=flood burst=10 nodelay;
proxy_pass http://origin_backend;
proxy_read_timeout 5s;
}
}
Advanced Evasion Patterns and Troubleshooting False Positives
Modern HTTP floods don't look like the old ab -n 100000 scripts. What we're seeing now are headless Chrome fleets — often 5,000 to 15,000 bots — that execute JavaScript, solve basic challenges, rotate through 20,000+ residential proxies, and randomize everything from TLS fingerprints to header order. They'll mimic human behavior: 2-3 requests per second, random 1-4 second delays, and they’ll even request CSS and images to look legitimate. If you're only fingerprinting on User-Agent or request rate, you’ll miss them entirely. You need to look at second-order signals like request entropy, cache-bypass rate (attackers almost always add ?rand= or Cache-Control: no-cache), and origin CPU time per IP.
Another pattern that's become brutal is the pulse-wave HTTP flood. Instead of a sustained 100k RPS, you'll get 250k RPS for 45 seconds, then silence for 90 seconds, then it hammers you again. This is designed to break auto-scaling and exhaust your incident response team. Traditional threshold-based alerting completely fails here because the average RPS looks normal. You’ve got to trigger on the delta — a 400% spike in 10 seconds — not just the absolute number. For a deeper dive on catching this, see our breakdown on DDoS Pulse Wave Detection: Identifying Intermittent Attack Bursts.
When you do tighten defenses, you *will* get false positives if you're not careful. The biggest culprit we've seen is mobile clients on high-latency networks triggering your body timeouts. Don't just 503 them. Serve a lightweight JS challenge or CAPTCHA instead of a hard block, and whitelist known good ASNs for your key markets. Monitor your 503 rate per PoP — if it jumps from a baseline of 0.05% to over 0.8% after a rule change, you've gone too far. Log everything with $request_time, $upstream_response_time, and your WAF's anomaly score so you can tune without guessing.
Frequently Asked Questions
How is an HTTP flood different from a Slowloris attack if both target Layer 7?
They attack different bottlenecks. Slowloris and RUDY are "low-and-slow" — they win by holding 1,000-2,000 connections open for minutes with minimal bandwidth (often <1 Mbps total), exhausting your web server's concurrent connection pool. An HTTP flood is "high-and-fast" — it sends 10,000 to 500,000+ complete, legitimate-looking GET/POST requests per second to overwhelm your application logic, database, and CPU. You'll need timeouts for the former and rate-limiting + caching for the latter.
Can a CDN or WAF alone stop these attacks?
A good CDN/WAF will absorb 90%+ of the pain, but it’s not magic. It can enforce timeouts, challenge suspicious clients, and cache static content so your origin never sees the flood. However, if your /login or /search endpoint is uncacheable and does a heavy DB query on every hit, even 2,000 RPS getting through the CDN can take down your origin. You still need origin hardening: connection limits, query optimization, and circuit breakers behind the CDN.
What's the safest way to test my own defenses against Slowloris?
Never test against production. Spin up a staging clone with identical timeout settings and use tools like slowhttptest or slowloris.py from a separate VPS. Start small: slowhttptest -c 1000 -H -g -o slowloris_test -i 10 -r 200 -t GET -u https://staging.example.com will simulate 1,000 sockets sending partial headers every 10 seconds. Monitor your netstat -an | grep :80 | wc -l and Nginx error logs. If connections stay ESTABLISHED for more than 15 seconds, your timeouts are too loose.
Why do Layer 7 attacks often show almost no increase in bandwidth graphs?
That's exactly why they're so dangerous and why they don't trigger volumetric alarms. A Slowloris attack holding 2,000 connections with a 15-byte header fragment every 15 seconds uses about 16 Kbps — invisible on a 10 Gbps graph. Even a 50,000 RPS HTTP flood with small 800-byte requests is only ~320 Mbps. Your network team sees nothing, but your Apache MaxRequestWorkers is maxed out and legitimate users are getting 503s. You must alert on application metrics: active connections, 5xx rate, and p95 latency, not just bits per second.