Slowloris: The DoS That Whispers

The Slowloris attack is from 2009. It fits in one small script, needs one machine and no bandwidth, and will still stall an out-of-the-box Apache today. It wins by going slow — and the fix is the same one HTTP/2's record-breaking floods needed.

The Slowloris attack is from 2009. It fits in a single small script, it needs one machine and essentially no bandwidth, and it will still stall an out-of-the-box Apache server today — seventeen years after Robert “RSnake” Hansen wrote it. Later that year, hacktivists reached for it to hit Iranian government sites during the post-election protests, precisely because it didn’t need a botnet or a fat pipe. A laptop on a bad hotel connection was enough.

I spent the last two posts on the loud end of denial of service — Rapid Reset firing 398 million requests a second, CONTINUATION Flood crashing a server from a single connection. Those are the firehose: exotic, recent, born inside HTTP/2’s new machinery. Slowloris is the opposite of all of it. No flood, no volume, no cleverness about frames. It wins by being slower than the server expects, and it has been winning that way since before HTTP/2 existed. That contrast is the whole reason it’s worth understanding.

A request that never ends

Here is an HTTP/1.1 request, the boring kind:

GET / HTTP/1.1\r\n
Host: example.com\r\n
User-Agent: Mozilla/5.0\r\n
\r\n

The request is not finished until that final blank line — the bare \r\n after the last header. That’s the terminator. Until the server sees it, it has to assume more headers are coming, so it holds the connection open and keeps a worker assigned to it, waiting to read the rest.

Slowloris simply never sends the blank line.

It opens a connection, sends the request line and the Host header, and then stops one keystroke short of done. It does not close the socket. Every fifteen or twenty seconds — comfortably inside whatever read timeout the server allows — it dribbles out one more meaningless header, X-a: b\r\n, just to reset the clock. The header is nonsense; its only job is to look like progress. As far as the server can tell, a slow but honest client is still typing its request. So it keeps waiting.

Now do that a few hundred times in parallel from one machine. Each connection costs the attacker almost nothing — a socket and a trickle of bytes a minute. Each connection costs the server a worker it can’t use for anyone else. Fill every worker with half-finished requests and the server has nothing left to answer real ones with. It isn’t crashed. It’s not out of CPU or bandwidth. It’s just completely, politely occupied, holding a room full of conversations that will never get to the point.

Why the server model decides everything

Whether this works comes down to one architectural choice: does the server dedicate a thread or process to each connection, or does it juggle many connections in an event loop?

Apache’s traditional model — the prefork and worker MPMs — assigns a worker to a connection and lets that worker block while it reads. It’s a clean, readable design, and it means the number of connections you can serve at once is capped at the number of workers you configured. Out of the box that ceiling is small, a couple hundred. Slowloris exists to fill exactly that many slots with connections that never complete. You don’t need to exhaust a big number; you need to exhaust a small one, and hold it.

Event-driven servers don’t work that way. nginx, and lighttpd before it, run a handful of worker processes that each watch thousands of connections at once, doing work only when a connection actually has data ready. An idle half-open connection sitting there sending a header a minute is nearly free to them — it’s just one more file descriptor in the poll set, not a whole worker parked on it. That’s why the standard advice for a decade has been to put nginx, or any buffering reverse proxy, in front of the thing you’re protecting. Not because nginx is magic, but because the connection-holding trick needs a server that assigns something scarce to each connection, and an event loop refuses to.

This is the same lesson from the Rapid Reset post seen from the other side. There, HTTP/1.1’s one-request-per-connection clumsiness was quietly doing security work by throttling attackers. Here, Apache’s worker-per-connection clarity is quietly creating the vulnerability, by making each connection expensive enough that a few hundred of them are fatal. The architecture that’s easiest to reason about is often the one with the most exploitable edges.

The fix is a stopwatch

There’s no protocol hole to patch in Slowloris, because nothing about it violates HTTP. Sending headers slowly is legal. Pausing between them is legal. The attack is just a client exercising its right to be slow, at scale, with no intention of finishing. So the defense can’t be “ban slow clients” — it has to be “stop waiting so patiently.”

Apache shipped exactly that as mod_reqtimeout, enabled by default since version 2.2.15 back in 2010. It sets a deadline for receiving the request headers and body, and a minimum data rate the client must sustain. A connection that sends a header a minute to keep itself alive now trips the limit and gets a 408 Request Timeout instead of a permanent seat. That single module turned Slowloris from “takes down default Apache” into “takes down Apache someone deliberately misconfigured.” A pile of other modules — mod_qos, mod_antiloris, mod_limitipconn, mod_evasive — attack it from the connection-count side instead, capping how many simultaneous connections one IP may hold, which is the other thing the attack depends on.

Notice that raising MaxRequestWorkers — giving the server more slots — is not on the list. It feels like the fix, and it’s a trap: the attacker just opens more connections, which cost them nothing, to fill the new slots, which cost you memory. You can’t out-provision an attack whose per-unit cost is a socket and a byte a minute. The only thing that works is refusing to hold a slot for a client that isn’t making real progress.

The whole family whispers

Slowloris starves the server on the request headers. Its siblings do the same trick on other parts of the exchange. RUDY — “R-U-Dead-Yet” — sends a POST with a large declared Content-Length and then trickles the body one byte at a time, holding the connection while the server waits for a payload that arrives at a byte a minute. The Slow Read attack inverts it: request something large, then advertise a tiny TCP receive window and read the response excruciatingly slowly, pinning the server’s send buffer. Same shape every time — declare an intention, then take forever to fulfill it, and make the server hold state the entire while. Collectively they’re the slow HTTP attacks, and the mitigation is always the same family: timeouts, minimum data rates, per-connection and per-IP limits, and a buffering proxy that reads the whole request itself before it ever bothers the origin.

Here’s the thread that runs through all four posts. Rapid Reset makes the server do work before a RST_STREAM it can’t take back. CONTINUATION Flood makes it hold header fragments waiting for an END_HEADERS that never comes. Slowloris makes it park a worker waiting for a blank line that never comes. Different decade, different protocol version, wildly different bandwidth — but the same underlying con every time: get the server to commit something scarce to a promise the client has no intention of keeping. The exotic HTTP/2 floods and a seventeen-year-old Perl script are, at heart, the same move.

Which is why the defense keeps rhyming, too. Every one of these is beaten by the same unglamorous instinct: don’t extend infinite patience to an unfinished request. Set a deadline. Demand progress. Hang up on the connection that keeps talking without ever getting to the point. The internet spends enormous effort making servers faster and more efficient, and almost none teaching them when to stop waiting — and stop waiting is the one defense the whisper and the firehose both fear.

Continue the conversation

← Back to Blog