Quick answer
An HTTP flood is a denial-of-service attack carried out with ordinary web requests. Instead of trying to saturate the network, the attacker sends requests that are expensive for the server to answer — a search query, a login attempt, a cart operation — until the application runs out of workers, database connections or CPU. Each request is individually valid, which is what makes it hard: there is no malformed packet to drop and no bandwidth spike to notice.
01Cheap to send, expensive to answer
The asymmetry is the whole attack. A GET request is a few hundred bytes and costs the attacker nothing. Answering it might mean a database query across a large table, rendering a template, and a round trip to a cache — tens of milliseconds of work.
At a few thousand requests per second, the traffic is trivial on the wire and completely overwhelming for the application. This is why an HTTP flood can take down infrastructure that would shrug off a hundred gigabits of junk packets.
It is also why the defences differ. The difference between Layer 3/4 and Layer 7 attacks is not only which layer is hit but which resource runs out, and capacity that absorbs packets does nothing for a database at its connection limit.
| Volumetric flood | HTTP flood | |
|---|---|---|
| Measured in | Gigabits per second | Requests per second |
| What runs out | Bandwidth | Workers, DB connections, CPU |
| Visible on a traffic graph | Obviously | Often not at all |
| Stopped by | Upstream capacity | Request inspection |
| Individually valid requests | No | Yes — that is the problem |
02The common variants
HTTP floods come in a handful of recurring shapes. They often run together, and the mix tells you what the attacker has learned about your site.
| Variant | What it does | What runs out |
|---|---|---|
| GET flood | Repeats requests for pages that cannot be served from cache | Application workers and database time |
| Cache-busting flood | Appends a random query string so every request misses the cache | The origin, which now answers what the cache should have |
| POST flood | Submits forms, searches or API calls the server must parse and process | CPU, request-body buffers, write capacity |
| Login flood | Hammers authentication, often mixed with credential stuffing | CPU spent on deliberately slow password hashing |
| Slow requests | Sends headers or bodies a byte at a time to hold connections open, as in a Slowloris attack | Connection slots rather than CPU |
| HTTP/2 stream abuse | Opens and cancels streams faster than the server can clean up, as in HTTP/2 Rapid Reset | Request handling, at enormous rates per connection |
03What attackers aim at
A competent attacker does not hit your homepage, because your homepage is probably cached. They look for the endpoints that cannot be cached and cost the most.
- Search endpoints — full-text queries across large tables
- Login — deliberately slow password hashing, by design
- Cart and checkout — session state, inventory locks, no caching possible
- Filtered listing pages, where every parameter combination is a fresh query
- Any URL with a random parameter appended, which defeats caching entirely
- API endpoints that fan out into several backend calls
04How defenders tell it apart
Since no single request is malformed, detection looks at behaviour across many requests rather than at the content of one. The broad families are common knowledge across the industry; what differs between providers is how they are combined and weighted, which nobody publishes, because a published list is a checklist for attackers.
- Consistency: whether the client behaves like the software it claims to be; TLS fingerprinting is the best-known industry example
- Browsing behaviour: real visitors move through a site in varied ways; floods repeat a few expensive URLs
- Request mix: a sudden shift in which endpoints are being hit, compared with what that site normally sees
- Distribution: many new sources converging on the same endpoint at the same moment
- Capability: whether the client completes a challenge that a real browser handles without the visitor noticing, such as a proof-of-work challenge
05Why rate limiting alone is not enough
The instinctive response is a per-address rate limit, and it does help against a single noisy source. Against a distributed attack it does very little: ten thousand bots making two requests a second each stay comfortably under any sane limit while delivering twenty thousand requests per second to your origin.
Rate limiting is still worth having — it stops the trivial cases and puts a ceiling on damage — but it has to be one layer among several rather than the whole defence.
06Find what is being hit
Before changing anything, find out which endpoint the flood is aimed at. The access log answers that in seconds. A path whose number of distinct URLs is close to its number of requests is being cache-busted, and an endpoint that has suddenly become the most requested on the site is the target.
Count the distinct clients as well. A few addresses doing most of the work can be limited individually; thousands of addresses doing a little each cannot, and that tells you the problem needs filtering in front of the server rather than rules on it.
# requests | distinct URLs | path: distinct close to requests means cache-busting
awk '{split($7,u,"?"); t[u[1]]++; if(!seen[$7]++) d[u[1]]++}
END{for(p in t) print t[p], d[p], p}' access.log | sort -rn | head
# who is hitting the target, and how many distinct clients there are
awk '$7 ~ /^\/search/ {print $1}' access.log | sort | uniq -c | sort -rn | head
awk '$7 ~ /^\/search/ {print $1}' access.log | sort -u | wc -l
# status codes on the login route: a wall of 401s is a login flood
awk '$7 ~ /^\/login/ {print $9}' access.log | sort | uniq -c | sort -rn07Per-route limits on the origin
Per-route limits on the expensive endpoints are the next step. They will not stop a distributed flood, but they stop one noisy client from occupying a whole worker pool, and they make the expensive paths cheap to refuse. The fuller set of limits, timeouts and caching rules is in nginx DDoS protection.
Answer refused requests with 429 Too Many Requests, the status RFC 6585 defines for rate limiting, rather than with a full error page. nginx returns 503 by default, which tells the client the whole service is unavailable rather than that this client is over its limit.
# Behind a proxy or CDN, restore the visitor address first
set_real_ip_from 203.0.113.0/24; # your provider's published ranges
real_ip_header X-Forwarded-For;
real_ip_recursive on;
limit_req_zone $binary_remote_addr zone=search:10m rate=2r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_req_status 429;
limit_conn_status 429;
upstream app { server 127.0.0.1:8080; }
server {
listen 80;
server_name example.com;
limit_conn perip 20;
location /search {
limit_req zone=search burst=10 nodelay;
proxy_pass http://app;
}
location = /login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://app;
}
location / {
proxy_pass http://app;
}
}08What works
- Challenge ambiguous traffic instead of blocking it, so a false positive costs a moment rather than a customer
- Cache what can be cached — those requests never reach the application at all
- Rate limit per route, with tight limits on expensive endpoints and loose ones elsewhere
- Layer the defences — caching, per-route limits, challenges and inspection — rather than relying on any one of them
- Keep protection always on, so it is already in place when the first request of a flood arrives
- Make refusals cheap: a short 429 or a cached error page costs far less than the page that was asked for
09Symptoms and first checks
When the site slows down and the network graph looks calm, these are the patterns to check first. To rule out a genuine spike or an unrelated origin fault before blaming a flood, see signs of a DDoS attack.
| Symptom | Likely cause | First check |
|---|---|---|
| CPU at 100%, bandwidth normal | HTTP flood on an expensive endpoint | Top paths in the access log for the last ten minutes |
| Cache hit ratio collapses | Cache-busting query strings | Distinct URLs per path |
| Database connection pool exhausted | Flood on search or filtered listings | The slow-query log and the paths behind it |
| Many open connections, few completed requests | Slow-request attack | Connection states on the web server |
| Spike of 401 or 403 on the login route | Login flood or credential stuffing | Failed logins per minute and the spread of usernames |
| Proxy shows 502 or 504, origin looks idle | Origin workers stuck waiting on a slow dependency | Worker counts and upstream timeouts on the origin |
Questions
Frequently asked questions
Sources
Sources and verification
nginx snippet checked with nginx -t (nginx 1.22.1, Debian package with the realip module) and exercised against a local upstream: a burst above the search limit received 429, while a second client and unlimited paths were unaffected; awk triage commands run with GNU Awk 5.2.1 against a crafted access log. Debian 12, October 2026.
- 01Google mitigated the largest DDoS attack to date, peaking above 398 million rps · Google Cloud
- 02RFC 6585: Additional HTTP Status Codes (429 Too Many Requests) · IETF
- 03Module ngx_http_limit_req_module · nginx.org
- 04Module ngx_http_limit_conn_module · nginx.org
- 05Module ngx_http_realip_module · nginx.org
Last updated