An HTTP flood attack contains nothing malformed. Every request is complete, valid, correctly framed HTTP, the kind your server was built to answer. Taken one at a time, they are indistinguishable from a customer checking a balance. Taken together, they are more work than your stack can do.
So this is the one attack class where your front-line defenses finally have a signal to act on, which shifts the question. It is no longer whether DDoS protection can see the traffic. It is whether the threshold was set to the right number, whether the endpoint being hit is cacheable, and whether the smallest limit in your chain is the one you think it is. Configuration answers, all of them, and configuration goes stale quietly.
The short version
- Nothing about it is invalid. The load comes from sustaining a high number of concurrent in-flight requests, so there is no signature to match and no malformed field to reject.
- The damage travels further than in any other class. It reaches past the web server to application threads and then the database connection pool, which is usually sized in the low hundreds.
- Your edge controls are decisive here, not blind. Rate limiting, scrubbing and caching can all stop this. Whether yours do comes down to numbers in a config file.
- Connection reuse changes what you are testing. Reusing sockets concentrates load on the request-processing path. Turning it off moves part of it back onto connection setup.
- Detection fails by drowning rather than by silence. Log volume rises with request volume, which is how ingestion caps get hit, events get dropped and the alert that mattered arrives as one of four hundred.
This is one in a series covering each of the ten simulation types Obsidio runs, one post per attack. They fail in genuinely different places, which is the reason a test programme needs coverage across the categories rather than volume in one of them. For how they fit together and how to structure a first run, start with our guide on how to test your DDoS protection.
The ten simulation types
One post each, grouped the way the platform groups them.
Connection exhaustion
Protocol-specific and scripted
- DNS Floodcoming soon
- Puppeteer Scriptfunctional load, not an attack
What an HTTP flood attack actually does
Obsidio’s HTTP flood runs several independent request loops on each worker. A loop fires one request, waits for the response, and fires the next the moment it arrives. No pause, no think time, no randomization: the configured method, URL and body go out exactly as given, every time.
The pressure comes from concurrency, not bandwidth. Twenty loops per worker across a run’s workers keeps a large number of requests in flight at the same moment, and each one occupies something on your side: a slot in the request queue, an application thread, a pooled connection, a query the database has to plan and run. A volumetric attack fills your pipe. This one fills your capacity to do work.
Picture twenty people standing at a shop counter at once, each asking for the same item again the instant they are served. The shop never gets a break.
Sending identical requests has one consequence worth naming: a cache in front of you can serve them. That is the useful part. If a plain repeated request never reaches origin, you have learned something real about your edge. If it reaches origin every time, you have learned something you can act on.
Which of your controls should have caught this
It is worth walking the whole chain a request passes through, because the answer is rarely the control people expect. Three groups matter: what sits in front, what absorbs whatever gets through, and whether anything tells you it happened.
Zone 1
Should any of these have stopped it?
- DDoS protection and scrubbing. It has a real signal here, so it should act. Whether it does depends on thresholds set against your normal traffic.
- Firewall and WAF. Sees complete, valid requests with nothing to match on. It contributes only where it also enforces a rate.
- Rate limiting. Most likely to decide the outcome. What it keys on, the rate it allows and the burst it forgives are the whole answer.
- Bot detection and challenges. Static agent string, no browser behaviour, so a challenge ought to catch it. Whether the hit endpoint sits behind one is the open part.
- CDN cache. A cacheable path absorbs almost all of this. A login form or an API call reaches origin every time.
Zone 2
What takes the hit?
- Load balancer or reverse proxy. Forwards valid requests, because that is its job. Its upstream pool becomes an early ceiling.
- Connection layer. Holds up well. Sockets open and retire normally, and with reuse on, a few carry many requests each.
- Web server. Worker and thread pools saturate, and once they do, new requests queue instead of failing.
- Application and database. This is where it bites. Requests cost CPU, and every query takes a connection from a pool far smaller than the request rate.
- Auto-scaling. It sees the spike and reacts, the opposite of the slow-attack case. Whether it reacts fast enough, and whether more app servers on one database pool help, is what a test shows.
Zone 3
Would you find out?
- Dashboards. Requests per second climbs visibly. This is the attack your graphs are good at.
- Logging and your SIEM. The inverse failure. Log lines multiply with request volume, so ingestion caps get hit, licence overage bites, events get sampled away, and the window you most need is the one that got truncated.
- Alerting. Something fires. Whether it is one actionable alert or several hundred correlated ones is a tuning question only load answers.
- On-call. Somebody gets paged, probably repeatedly. Whether the page says enough to act on is the part worth testing.
- A check from outside your network. Reliable here. It settles whether users are affected while internal metrics are still being argued about.
Purple marks what only a test on your own infrastructure can settle. Everything else follows from how the attack works, not from how well your team configured things.
That third group is the part teams discover late. A control that works but is invisible is still a finding, and so is a control that reports more than anyone can read.
The parameters, and the connection reuse tradeoff
Five settings shape the run, and each moves the load somewhere different:
- Parallel requests per worker. Default 20, adjustable from 1 to 100. Raising it raises concurrent load and achievable request rate roughly linearly. The platform estimates five requests per second per loop, assuming the target answers within 200 ms.
- Method. Default
GET, withPOST,PUT,DELETEandHEADavailable. A body is sent only on the non-GET, non-HEADmethods, so a large payload shifts cost toward upload bandwidth and body parsing.HEADremoves the response-body cost from both ends. - Payload. Empty by default. Placeholders such as
{{uuid}}and sized random text expand per request, so bodies can differ while the request path stays constant. - Headers. Static headers on every request. The default sends only a user agent identifying the simulation, and anything can be overridden, which is how auth tokens and cookies get in.
- Connection reuse. On by default.
That last one deserves its own paragraph. With reuse on, a socket goes back to the pool after each response, so the next request skips the TCP and TLS handshake and the load lands almost entirely on request processing. Turn it off and every request opens a fresh socket: achievable rate drops and part of the load moves onto connection setup, which tests something else.
Your server, not the client, decides how long a pooled socket survives. nginx defaults to keepalive_timeout 75s and keepalive_requests 1000, so a connection closes after a thousand requests or 75 idle seconds whatever the client would prefer. Pairing reuse with a server that answers Connection: close builds a pool torn down after every response, which is overhead on both sides.
How this differs from GoldenEye and Browser Flood
All three sit in the application-layer flood category and they are not interchangeable. GoldenEye appends randomized query-string parameters and rotates the user agent, so it reaches origin even behind a CDN and blunts header-based filtering. HTTP flood does the opposite on purpose: identical requests, so your cache is allowed to do its job and you get to measure whether it does. Browser Flood drives a real Chromium tab on each worker and fetches the whole sub-resource graph a browser requests.
Run HTTP flood for raw request-processing capacity, GoldenEye to bypass the cache deliberately, Browser Flood for realistic user load.
How to defend against an HTTP flood attack
Defending against this is mostly arithmetic. Four things carry most of the weight:
- Rate limiting with a burst you have actually chosen. On nginx that is
limit_req_zoneto define the key and rate, thenlimit_req zone=name burst=numberwhere you apply it. Theburstparameter defaults to 0, so nothing above the rate is forgiven, and rejected requests get a 503 by default vialimit_req_status. Pick both values deliberately. - A concurrency cap alongside the rate cap.
limit_connlimits simultaneous connections per key, which addresses a different failure than a request rate does. It also answers with a 503 by default. - Caching you have confirmed. nginx ships
proxy_cache off, so caching exists only where somebody switched it on, andproxy_cache_lockalso defaults to off, meaning concurrent misses on the same key each go to origin separately. Under a flood that turns one cold object into hundreds of origin requests. Switching the lock on collapses them into one. - Know the smallest number in your chain. nginx allows
worker_connections 512per worker process by default. Apache’sMaxRequestWorkersdefaults to 256 on prefork and to 400 on the event and worker MPMs, which isServerLimitof 16 timesThreadsPerChildof 25. PHP-FPM’spm.max_childrenhas no default at all, is mandatory, and caps simultaneous requests served. PostgreSQL’smax_connectionsis typically 100. Your real capacity is the lowest of those, and it is very often the database.
Every value above lives in a file somebody edited once, which is why reading the config is weaker evidence here than running the attack.
What you can check yourself, and what it will get wrong
Three of these questions you can answer today without help. It is worth knowing which, and where a cheap check misleads you.
Read the configuration and do the arithmetic. Write down the rate limit and its key, which paths are cached, the worker ceiling, the process pool, the database connection limit. The smallest number is your capacity. That tells you what the numbers are. It does not tell you the chain behaves the way the arithmetic suggests, because queuing, retries and timeouts interact in ways no config file shows.
Test alert routing on its own. Fire a synthetic alert and see whether it reaches the right rotation and whether anyone acts. That verifies the plumbing, and it needs no attack traffic. It says nothing about how the same plumbing behaves when the volume is real.
Run a single-source load test against a staging environment, with change-management sign-off, on infrastructure you own. That is legitimate, and for capacity curves it is genuinely useful. It is also where the trap is.
One machine does not merely fail to answer the questions in the first zone. It answers several of them wrongly, in a reassuring direction. A per-source rate limit catches a single origin immediately, bot detection flags one client behaving oddly, reputation filtering handles it, and the edge gets declared sound.
A real distributed flood does not present that way. Thousands of residential addresses each send a modest, plausible number of requests, no single source crosses any per-source threshold, and the aggregate lands on your origin regardless. The controls you most need to trust are the ones a single-source test grades too generously.
Detection has the same asymmetry. A small test produces a trickle of logs, so it cannot say whether your ingestion cap gets reached, whether events start getting dropped, or whether the on-call engineer can find the useful alert among the rest.
How Obsidio tests an HTTP flood
Obsidio runs this as a controlled, authorized simulation against infrastructure you own. Domain ownership is verified by DNS TXT record before any traffic flows, runs ramp up gradually, and any run can be aborted live. You set the loops per worker, the method, the payload and whether connections are reused, then watch your stack. Load comes from 100,000+ globally distributed real devices, so per-source limits and reputation filtering get tested the way a distributed attack tests them.
Three things to watch during the run:
- Compare the counts along the chain. Requests accepted at the edge, requests reaching origin, requests completed successfully. Where those numbers diverge, a control is doing something, and the gap says more than any single figure.
- Watch the database pool, not the CPU. Application CPU can look survivable while queries queue for a free connection. Pool utilization and queue depth show where the ceiling really is.
- Measure from outside. Server-side metrics can read as degraded but acceptable while a real request from outside your network is timing out.
Every request flows through a response analyzer, so each bucket report carries what the edge did: WAF responses, scrubbing behaviour, rate-limit rejections, captcha challenges. Obsidio measures that, because it is visible from outside. Whether your SIEM ingested the event and whether anyone was paged are answers only your own consoles hold, so have someone watching them during the window.
Strength is proven, not promised. An HTTP flood does not look for a flaw in your code. It asks what the smallest number in your stack is, and whether anybody has checked it lately.
Where this fits in a testing programme
An HTTP flood is close to the exact inverse of Slowloris. The slow attack sends almost nothing, walks past volume-based defenses, stops at the web server’s connection pool, and produces silence where you would look for it. The flood is loud, gives your edge controls something to act on, reaches the database, and buries the finding in telemetry. Hardening for one teaches you little about the other. A programme built on floods alone leaves the slow class unmeasured, and one built on slow attacks alone never finds its capacity ceiling.
For regulated institutions, the test also has to leave a record. Obsidio produces cryptographically attested, tamper-evident reports mapped to FINMA, DORA and NIS2, generated inside Trusted Execution Environments, so the result of a run is an artifact you can file rather than a screenshot you have to vouch for.
To scope an authorized HTTP flood simulation against your own infrastructure, see the Obsidio platform or get in touch with the team. Independent, neutral, verifiable: resilience you can prove.
