Volume is not the only way to take a server down
Ask anyone to picture a DDoS attack and they picture a flood: gigabits per second, a graph going vertical, a pipe filled until nothing else fits. That picture is accurate for volumetric attacks, and it is why most defenses — and most dashboards — are built to spot a spike.
A low and slow attack never makes the spike. It targets a different limit entirely: how many requests your server can have in flight at once. Every web server has a finite pool of workers, threads, processes or file descriptors, and every one of them is occupied for as long as a request is unfinished. An attacker who can keep a request unfinished forever only needs enough connections to fill that pool. On a default origin configuration, that number is often in the hundreds — small enough for one machine on a home connection to reach.
The result is an outage that reads like a bug. The site hangs or times out, the server is not short of CPU, bandwidth sits near its usual baseline, and the access log has gone quiet — because the requests that are killing you were never completed, so they were never logged.
The three families of low and slow attack
All of them exploit the same assumption: that a client which opened a connection intends to finish using it.
| Attack | What it drips | What it exhausts | Classic target |
|---|---|---|---|
| Slowloris (slow headers) | One header line every few seconds, never the blank line that ends the request | Connection and worker pool | Thread-per-connection servers |
| R.U.D.Y. (slow POST / slow body) | A body byte at a time under a large declared Content-Length | Request-handling workers, application threads | Any endpoint accepting POST — forms, uploads, APIs |
| Slow read | Nothing — it advertises a tiny TCP receive window and drains the response slowly | Send buffers, connection slots, memory | Endpoints returning large responses |
Slowloris opens many connections and sends a valid but incomplete request on each: the request line, then a header, then silence. Just before the server's header timeout expires, it sends one more header line. The request is never terminated, so the server keeps waiting politely, and the connection is held for as long as the attacker cares to keep dripping. OWASP catalogues the family as slow-rate denial of service.
R.U.D.Y. ("R-U-Dead-Yet") does the same thing one step later in the request. It submits a form or API call declaring a large Content-Length, then delivers the body a byte or two at a time. The server has been told exactly how much data is coming, so it waits for all of it — and in most stacks the application worker is already assigned to that request while it waits.
Slow read inverts the trick. The request is normal and completes fine; the attacker just refuses to read the answer, advertising a near-zero TCP receive window so the server cannot flush its send buffer and cannot release the connection. Requesting your largest asset repeatedly this way ties up memory as well as slots.
What they share is the economics. Each connection costs the attacker one socket and a few bytes a minute, while costing you one worker. That is an exchange rate no amount of upstream bandwidth can fix.
Which stacks are exposed
The blunt version: anything that dedicates a worker to a connection for the whole duration of a request.
- Thread- or process-per-connection servers are the classic victims — Apache's prefork and worker MPMs, older Tomcat connectors, and most application servers you run directly on a port. When
MaxRequestWorkersconnections are held open, the next real visitor queues behind them. - Event-driven servers — nginx, Node.js, Go, and Apache's event MPM — handle idle connections far more cheaply, which is why "nginx is immune to Slowloris" gets repeated. It is closer to true than false, but only about the front door: nginx still has
worker_connectionsand a file-descriptor ceiling, and the moment a request is proxied onward, whatever sits behind it may be a PHP-FPM pool with a few dozen children. Fill that pool and the site is down regardless of how efficiently the proxy is waiting. - Application tiers with fixed pools are usually the real bottleneck: FPM children, Puma or Unicorn workers, a database connection pool, a Java thread pool. These are sized for normal concurrency, which is a much smaller number than most people assume.
HTTP/2 and HTTP/3 changed the arithmetic without retiring the class. Many slow requests now share one connection as separate streams, so per-connection limits catch less, and the relevant ceiling becomes the concurrent-stream limit and the memory held per stream. Slow-rate behaviour still works; it just needs fewer sockets.
Why your monitoring says everything is fine
This is the part that costs teams the most time, because the metrics people watch are the metrics designed to catch a flood.
| Signal | Under a volumetric flood | Under a low and slow attack |
|---|---|---|
| Bandwidth | Saturated | Normal, sometimes below normal |
| Requests per second | Far above baseline | Flat or falling |
| CPU on the origin | High | Often idle — the workers are waiting, not working |
| Concurrent connections | High | High, and climbing steadily |
| Average request duration | Normal-ish | Rising toward your timeout values |
| Access log | Flooded | Quiet: unfinished requests are never logged |
So look at the right things. Concurrent connections in state ESTABLISHED against your web port, your server's busy-worker count (Apache's server-status, nginx's stub_status active connections, your FPM pool status), and the distribution of request duration rather than its average. A low and slow attack shows up as a long, flat plateau of open connections that never resolve, spread thinly across many source addresses, each sending a trivial number of bytes.
The tell that removes all doubt: your origin is unreachable through the browser while it still answers instantly on an unrelated port over SSH. Nothing is overloaded. Everything is occupied.
First aid at the origin: timeouts and connection limits
If you are being held open right now, the fastest lever is to stop being patient. Every one of these attacks depends on your server tolerating a request that makes no progress.
On nginx, tighten the clocks and cap concurrency per client:
client_header_timeoutandclient_body_timeout— how long a client may take to send headers and body (default 60s each; single-digit seconds is realistic for most sites).send_timeout— how long a client may stall between reads of the response, which is what bites slow-read attacks.keepalive_timeout— how long an idle connection may linger after a completed request.limit_connwith a zone keyed onbinary_remote_addr— a hard ceiling on simultaneous connections per source address.
On Apache, mod_reqtimeout is the direct answer and ships enabled in modern versions; set an explicit header and body timeout with a minimum data rate so a trickle no longer counts as progress. Raising MaxRequestWorkers buys headroom, and switching from prefork to the event MPM changes the shape of the problem, but neither is a fix on its own — the attacker's cost of adding connections is far lower than your cost of adding workers.
Two more that help and are worth doing anyway: put a reverse proxy in front of any thread-per-connection application server so the proxy absorbs the waiting and forwards only complete requests, and make sure request bodies are fully buffered before they reach your application.
Then treat all of it as first aid. It raises the cost of the attack; it does not end it.
Why origin tuning is not the whole answer
Aggressive timeouts collide with a fact of the real internet: some of your legitimate clients genuinely are slow. A phone on a weak mobile signal, a customer uploading a large file, a webhook sender on a congested link, a browser on hotel Wi-Fi — all of them can look like a slow drip if you set a five-second body timeout and stop thinking about it. Tune too far and you have implemented the denial of service yourself, quietly, against exactly the users least able to retry.
Per-IP connection limits have the same problem from the other direction. Twenty connections from one address is aggressive for a script and completely normal for an office, a mobile carrier NAT or a university. And a distributed low and slow attack — the same technique spread over a botnet, one or two connections per host — never trips a per-IP limit at all, because no single source is doing anything unusual. That is the version that shows up against sites that have already been hit once and hardened, and it is why this belongs in the same category as any other application-layer attack: the individual client looks fine, and only the population is wrong.
What actually stops it: terminate and buffer at the edge
The structural fix is to make sure your origin never holds an unfinished request in the first place.
When a reverse-proxy edge sits in front of your site, every client connection terminates there. The edge waits out the slow client on its own event-driven infrastructure — where an idle connection costs a few kilobytes rather than a worker — and only opens a connection to your origin once it holds a complete request. From your server's point of view, every request arrives instantly and finishes immediately. There is no state for a slow client to occupy, because slow clients are somebody else's problem, on hardware built to hold millions of them.
That single property does most of the work. The rest comes from things the edge can see and your origin cannot:
- Behavioral analysis across the whole population — one client sending a header every 9 seconds is unremarkable; ten thousand doing it in the same rhythm is a signature, and it is visible only to something watching all of them at once.
- Client fingerprinting — TLS and HTTP-level signatures that identify the tooling behind a connection regardless of the user agent it claims, which is how a slow-drip script gets separated from a genuinely slow phone.
- Per-client concurrency and request budgets applied at the edge, keyed on something better than a shared IP address.
- Edge caching — every cache hit is a request your origin never has to hold open at all, which is why a CDN is also attack armor.
This is the same argument as always-on mitigation generally, with one extra edge case that matters here: low and slow attacks are quiet enough to slip under the activation thresholds that on-demand scrubbing relies on. An attack that never produces a traffic spike may never trigger the diversion that was supposed to save you.
How Itnetic stops low and slow attacks
Itnetic is a reverse-proxy edge, so the structural defense above is the default rather than a setting: connections terminate at the point of presence nearest the client, requests are buffered, and your origin only ever sees complete requests from our network. A Slowloris connection is held open against edge infrastructure designed to hold connections open, and your server never learns it happened.
On top of that:
- Always-on filtering. There is no threshold to cross and no switch to flip, which matters most for attacks whose entire design is to stay under thresholds.
- Adaptive Layer 7 detection builds a behavioral baseline per host and endpoint, so the coordinated rhythm of a slow-drip botnet stands out even though its volume never does — and load shedding keeps the edge responsive while it filters.
- Per-request logs capture status, latency, TLS details, client fingerprint and the rule and verdict behind every decision, so a slow attack leaves the paper trail your origin's access log structurally cannot — see logs and analytics and the attack timeline that marks the second mitigation engaged.
- API zones keep machine clients working while all of this happens: rate-limited API callers get a
429withRetry-Afterinstead of a challenge page they cannot solve — the reasoning is in protecting an API from DDoS attacks. - Custom WAF rules run in log-only mode first, so you can watch what a rule would have caught on live traffic before it blocks anything — the safe way to tighten limits without discovering your slowest legitimate client the hard way.
- Attack traffic is never metered against bandwidth quotas (pricing), and the Starter plan is free. Going live is two DNS records and about five minutes.
One thing to do regardless of provider: lock down the origin. Edge mitigation only sees traffic that comes through the edge, and an origin IP still answering the internet directly can be dripped to death from anywhere — the checklist is in how to hide your origin IP address.
If it is happening now
Cap concurrent connections per source and cut your header, body and send timeouts to values a real browser meets comfortably; that alone often restores service long enough to think. Confirm the shape of it by counting open connections and bytes per connection rather than by looking at bandwidth. Then get the origin behind an edge that will terminate connections for you, and firewall it so the old address stops answering.
The full incident sequence is in how to stop a DDoS attack. When it is over, verify the fix rather than assuming it — how to test your DDoS protection includes the connection-behaviour checks that catch this class before it catches you.