Skip to content

Slowloris and the slow-attack family: starving a server with patience

· 23 min read
Copyright: MIT
The word SLOWLORIS as a large monospace wordmark with one orange partial HTTP header line trailing off, never terminated

A flood works by being loud. You point enough traffic at a target that the pipe fills, the network card saturates, or the upstream router gives out, and the volume itself is the weapon. That is the shape almost everyone pictures when they hear “denial of service”: gigabits, then terabits, of packets arriving faster than anything can absorb them. It is the wrong picture for a whole class of attacks that does the opposite. Some of the most effective denial-of-service techniques against a single web server send almost nothing. A few hundred connections, a trickle of bytes per second, a residential uplink doing nothing a packet capture would call abnormal. And the server falls over anyway.

The reason it works is that a web server is not only a bandwidth machine. It is also a finite pool of slots, each one able to handle exactly one request at a time, and each one willing to wait politely for a slow client because slow clients are real. Phones on bad cellular links. Users on satellite. A connection from the other side of the planet. The patience that makes a server usable on the open internet is the same patience an attacker can abuse. Hold every slot open, feed each one just enough to keep it from timing out, and the pool empties for everyone else. This post is about that family of attacks: how Slowloris holds a request open by never finishing the headers, how slow POST does the same trick with a request body, how slow read flips the direction and starves the server on the way out, and why the architecture of the server, thread-per-connection versus event loop, decides whether any of it works.

The sections below start with the resource that all of these attacks actually exhaust, then walk through the three variants in turn (slow headers, slow body, slow read), then the mechanism that separates the servers that fall over from the ones that don’t, and finally the defenses that actually time these connections out and why a reverse proxy is the cleanest of them.

The resource being exhausted is the connection slot, not the bandwidth

Start with what a web server holds open while it talks to you. A request is not instantaneous. The client opens a TCP connection, the server accepts it, and then the server reads the request line and the headers, waits for the body if there is one, builds a response, and writes that response back. For the entire span from accept to the last byte written, that connection occupies something: a process, a thread, a slot in a connection table, an entry in an event loop. The server has a finite number of those somethings. When they are all in use, a new client that connects gets queued, or refused, or simply hangs.

On a classic thread-per-connection server the something is heavyweight. Apache’s prefork multi-processing module dedicates an entire OS process to each connection. Its worker MPM is lighter, threading multiple connections inside a smaller number of processes, but a connection still owns a thread for its whole lifetime, and that thread is blocked, sitting in a read call, whenever it is waiting for the client to send more. The ceiling is set by a configured maximum: in older Apache the directive was MaxClients, later renamed MaxRequestWorkers, and a common default sat in the low hundreds. That number is the whole game. If an attacker can occupy every worker and keep it occupied, the server is down for everyone, and the attacker never has to send a flood. It just has to send slowly, and never stop.

This is the insight RSnake’s tool turned into a weapon, and it is worth stating plainly before the variants, because all three share it. The attacker is not racing the server’s bandwidth. The attacker is racing the server’s timeout. As long as each connection delivers a few bytes more often than the idle timeout fires, the server keeps waiting, and the slot stays held. A few bytes every ten or fifteen seconds is enough. The math is brutal: if a server has 256 workers and an attacker can hold 256 connections at a cost of, say, one packet every ten seconds per connection, the attacker’s total send rate to take the server offline is on the order of a couple of hundred bytes per second. That is not a typo. The bandwidth budget for this attack is smaller than the bandwidth of a single voice call.

Volumetric flood fills the pipe many gigabits, network saturates

Slow attack fills the slots x x x x x x a few hundred bytes/sec, all workers held no free slot for a real client Same outcome. One needs a botnet. The other needs a laptop. Two roads to the same denial of service: a flood exhausts the link, a slow attack exhausts the worker pool with almost no traffic.

That difference in resource is why these attacks evade the defenses built for floods. A rate limiter counting requests per second sees a handful of connections that each send one slow, well-formed request. A bandwidth-based detector sees nothing close to a threshold. From the outside the traffic looks like a small number of unusually patient users, which is exactly what it is pretending to be. If you want the broader contrast with the loud end of the spectrum, the layer-7 DDoS and SYN flood writeups cover the attacks that do try to win on volume.

Slowloris: the request that never ends its headers

Slowloris was the first of these to get a name and a tool. Robert Hansen, who wrote under the handle RSnake, released it on June 17, 2009. The name is a joke about the primate; the attack is about HTTP’s willingness to wait for a request to finish.

The mechanism rides on a structural fact about HTTP/1.x. A request’s headers are terminated by a blank line, the sequence carriage-return, line-feed, carriage-return, line-feed, written as \r\n\r\n. The server reads header bytes until it sees that terminator, and only then does it consider the request header complete and move on to routing it. Until the terminator arrives, the server is in the middle of reading a request. It cannot dispatch the request, because it does not yet know what the request is. It cannot close the connection, because by every rule it has, this is a live client mid-transmission that simply has not finished yet. So it waits.

Slowloris opens a connection, sends a valid request line and the start of the headers, and then never sends that terminating blank line. Instead it dribbles. Every so often, before the server’s header-read timeout would fire, it sends one more header line, something innocuous, a made-up X- header with a random value. That single line resets nothing about the request’s completeness but it is data, and on a server that resets its idle timer whenever data arrives, fresh bytes mean the connection stays alive. The request is never completed and never abandoned. It just hangs there, holding its worker, forever, or until the attacker decides to let go.

attacker server worker GET / HTTP/1.1 Host: target\r\n

state: reading headers

X-a: 8341\r\n (10s later) X-b: 1907\r\n (10s later) X-c: 5562\r\n (10s later)

the blank line \r\n\r\n never arrives worker stays blocked; multiply by a few hundred connections Each orange line is a single partial header sent just before the timeout. The request is held open indefinitely because it is never completed and never idle.

A few details decide whether a given server is vulnerable. The server has to read headers incrementally and apply only an idle timeout (reset on each byte) rather than a hard ceiling on total header time. It has to dedicate a worker to the connection for the duration of that read. And the per-source connection cap has to be loose enough that one IP can open dozens or hundreds of connections. RSnake’s own notes listed Apache 1.x and 2.x, dhttpd, and others as affected, and named several servers that handled it gracefully, including IIS, lighttpd, and Cherokee, because their connection handling did not pin a thread per slow reader. Apache was the headline target precisely because its default MPMs were thread- or process-per-connection.

There is a wrinkle worth flagging honestly. Slowloris is an HTTP/1.x attack in its classic form. HTTP/2 multiplexes many logical streams over one TCP connection, so the per-connection accounting is different, and HTTP/2 has its own slow-and-low variants that abuse stream state and flow control rather than incomplete headers. The HTTP/2 Rapid Reset attack is a different beast, fast rather than slow, but it lives in the same neighborhood of “abuse the protocol’s own state machine instead of the link.” For HTTP/1.1 origins, which are still everywhere behind proxies, the classic Slowloris mechanism is unchanged from 2009.

Slow POST and RUDY: starving the server on the request body

Slowloris holds the headers hostage. The natural sibling holds the body hostage, and it is in some ways cleaner, because HTTP hands the attacker a number to lie with.

When a client sends a request with a body, a form submission, a file upload, an API call, it tells the server in advance how many bytes to expect. In HTTP/1.1 that is the Content-Length header. The client declares, say, Content-Length: 1000000, and the server now knows it must read a million bytes of body before the request is complete and before it can hand the request to the application. A well-behaved client then sends those bytes promptly. A slow-POST attacker sends them one at a time, with long pauses, the same dribble as Slowloris but applied to the body. The headers are complete and valid, so anything that only guards the header phase waves the request through. Then the body arrives at a byte every several seconds, and the worker sits there reading it, slot held, for as long as the attacker cares to keep going.

This technique has a tangled history that is worth getting right, because the names get used interchangeably. The slow HTTP POST method was discovered by Wong Onn Chee around 2009 and presented, with Tom Brennan, at OWASP AppSec DC in November 2010, which is also where the OWASP HTTP POST tool came from. RUDY, “R-U-Dead-Yet,” is the proof-of-concept tool that made the technique famous under a memorable name; its lineage traces to Raviv Raz, and it implemented the attack by walking through a target’s web forms, finding POST endpoints, and submitting long form fields one byte at a time. The original tool’s behavior, as documented since, breaks the body into very small chunks, on the order of a single byte, and sends them at intervals on the order of ten seconds.

Phase 1 — headers (sent fast, fully valid) POST /upload HTTP/1.1 Host: target Content-Length: 1000000

Phase 2 — body (sent one byte every ~10s) 1 byte …wait… 1 byte …wait… 1 byte …wait…

Declared 1,000,000 bytes. Delivered <100. The server is obligated to keep reading. Header-phase timeouts do not fire — the headers were perfect. The lie lives in Content-Length. Valid headers buy the attacker past header-stage defenses, then the body trickles in forever against a length that will never be met.

The reason slow POST is sometimes more dangerous than Slowloris in practice is that it targets the part of the request most defenses ignore. Plenty of hardening guidance, and plenty of WAF rules, focus on header-read time, because that is the Slowloris surface. A slow body sails past all of that with a clean header block. It also targets endpoints that accept large bodies by design, upload handlers, import endpoints, anything where a one-megabyte Content-Length is unremarkable, so the declared size does not even look suspicious. The defense has to be a timeout or minimum-rate check on the body phase specifically, which is exactly the distinction the modern Apache directive makes, as we will see.

Slow read: flipping the direction and starving the server on the way out

The first two attacks slow down what the client sends. Slow read slows down what the client receives, and it is the most underhanded of the three because the slowness lives below HTTP entirely, in TCP’s flow control.

TCP has a built-in mechanism for a receiver to say “stop, I’m full.” Every TCP segment carries a receive-window value: the number of bytes the receiver currently has buffer space for. If the receiver advertises a small window, the sender may only put that many unacknowledged bytes on the wire before it has to wait. If the receiver advertises a window of zero, the sender must stop completely and wait for a window update before sending another byte of data. This is normal, healthy, and happens constantly on real connections when an application reads slower than the network delivers. It is also exactly the lever a slow-read attacker pulls.

The attacker requests a genuinely large resource, something whose response will not fit in the server’s socket send buffer, then refuses to read it. Concretely, the client advertises a tiny receive window from the start. In one documented capture the initial SYN advertised a receive window of just 28 bytes. The server sends what little fits, fills the window, and then the client stops draining its own receive buffer, so its advertised window stays at or near zero. From the server’s side, it has a response to deliver, it has written everything the window allowed, and now it is blocked, waiting for the client to make room. TCP does exactly what it should: it holds the data, and it periodically sends a zero-window probe to ask whether space has opened. The server keeps polling at backing-off intervals, out to a couple of minutes between probes, and it holds the connection, and the worker, the whole time. Nothing is wrong at the protocol level. The client is “just slow.”

attacker server GET /big-file (win=28) response data (only 28 bytes fit) ACK win=0 (buffer never drained) zero-window probe (1s) zero-window probe (2s, 4s, ... up to ~2min)

server holds the response, and the worker, indefinitely Slow read inverts Slowloris: the attack is in the response path, hidden inside TCP’s own zero-window flow control, where nothing looks malformed.

What makes slow read nasty for defenders is that the usual hardening does not touch it. Header-read timeouts already passed. Body-read timeouts do not apply, there is no request body. The connection is in the write phase, and many servers historically applied no ceiling at all to how long a response may take to flush, because a slow reader on a bad link is a legitimate thing. The two requirements for the attack are that the requested resource be larger than the server’s socket send buffer (the buffer is commonly tens to low hundreds of kilobytes, and a typical modern page easily exceeds that) and that the server not impose a maximum write time. Both held on a lot of stock configurations.

All three variants got packaged together in one place. Sergey Shekyan at Qualys released slowhttptest in 2011, an open-source tester that implements slow headers, slow body, and (added in version 1.3, in early 2012) slow read in a single tool, explicitly crediting RSnake’s Slowloris and Brennan’s OWASP slow-post work as predecessors. The point of a tool like that is defensive: you run it against your own server to find out how many concurrent slow connections it tolerates before it stops answering. In one of Shekyan’s own demonstrations a stock server given 1000 connections at 200 per second could service only a few hundred at once and went unresponsive within about five seconds. This post will not reproduce attack invocations; the mechanism above is the part worth understanding, and the tool’s own documentation covers the rest for people testing their own infrastructure.

Why thread-per-connection servers fall and event-driven ones don’t

Everything so far comes down to one architectural choice: what the server does with a connection that is waiting. There are two answers, and they decide the outcome.

The thread-per-connection model gives each connection an OS thread or process for its entire life. When that connection is waiting for the client, to finish its headers, to send the next body byte, to drain its receive buffer, the thread is blocked inside a system call, doing nothing but occupying memory and counting against the worker limit. This is simple to reason about and was the dominant model for years. Apache’s prefork and worker MPMs work this way. The vulnerability is structural: a blocked thread is a consumed thread, and slow attacks exist precisely to keep threads blocked. Raise the worker count and you raise the attacker’s required connection count proportionally, but you also raise your memory footprint, and the attacker’s cost per connection is so low that they win the arithmetic almost every time.

The event-driven model decouples connections from threads. A single worker process runs an event loop, on Linux typically built on the epoll system call, that watches thousands of socket file descriptors at once and wakes up only for the ones that have data ready. A connection that is sitting idle, mid-Slowloris, costs a file descriptor and a small bookkeeping structure, but it does not hold a thread, because there is no thread assigned to it. The same handful of worker processes step through whichever connections actually have work. Nginx was built this way from the start, and it is the standard example of a server that absorbs slow attacks without falling over, not because it detects them, but because the resource they target, the dedicated thread, does not exist in its design. Holding ten thousand idle connections open against an event-driven server costs ten thousand cheap structures, not ten thousand threads.

Thread per connection one blocked thread per slow connection thread 1 BLOCKED thread 2 BLOCKED thread 3 BLOCKED thread N BLOCKED pool exhausted at N. real clients: queued / refused.

Event loop (epoll) idle connection = one fd, no thread grey = idle (free), orange = ready (has data) few workers loop workers touch only ready fds. idle costs ~nothing.

The slow attack targets a thread. If there is no thread to target, there is no attack. caveat: an event server still has fd and memory ceilings, and a slow upstream can still back up. The architecture is the whole story. A blocked thread is the resource slow attacks exhaust; an event loop simply has no per-connection thread to block.

This is not a clean win for event-driven servers in every dimension, and it is worth being precise. An event loop still has limits: file-descriptor ceilings, memory for connection state, and worker-connection caps like nginx’s worker_connections. Pile on enough connections and even an event server runs out of descriptors. And the modern Apache event MPM closes most of the classic gap by handling keep-alive and waiting connections in an event-driven listener thread, freeing the worker thread while a connection waits, so “Apache” in 2026 is not automatically the soft target it was in 2009. The clean statement is narrower than “nginx good, Apache bad.” It is that a server which assigns a dedicated, blocking thread to a connection for the whole time that connection is waiting is the one slow attacks were designed to kill, and the further a server’s design moves from that, the less these attacks do.

What actually times these connections out

Defending against slow attacks is not about detecting malice. It is about refusing to wait forever. Every effective defense is, at bottom, a clock: a limit on how long a connection may take to make progress, or a limit on how many connections one source may open, or a layer in front that will not pass an incomplete request through at all.

On Apache, the canonical fix shipped as a module. mod_reqtimeout arrived in version 2.2.15 and became the official answer to slow-header and slow-body attacks. Its directive, RequestReadTimeout, puts separate clocks on the header phase and the body phase, and crucially it expresses them as a starting timeout plus a minimum data rate rather than a flat deadline, so a genuinely slow but honest client is not punished. The default in current Apache reads:

RequestReadTimeout handshake=0 header=20-40,MinRate=500 body=20,MinRate=500

Read that as: give the client 20 seconds to start sending headers, and extend the allowance as more header data arrives, up to a hard ceiling of 40 seconds, as long as it sustains at least 500 bytes per second; give the body 20 seconds with the same 500-byte-per-second floor. A Slowloris connection dribbling one header line every ten seconds is nowhere near 500 bytes per second, so its clock runs out and Apache returns a 408 Request Timeout and reclaims the worker. The MinRate mechanism is the clever part: it is not a fixed deadline a slow client trips over, it is a rate floor an honest client clears and an attacker, by definition, cannot. The handshake stage, which guards the TLS handshake itself, was added later in 2.4.39.

Slow read needs a different clock, because it lives in the response phase that RequestReadTimeout does not cover. The defenses there are a maximum response-transmission time, a minimum send rate to the client, and caps on how many connections a single IP may hold. Modules like mod_qos and connection-limiting modules address the connection-count angle; some WAF and proxy layers add an explicit write timeout. The general principle is the same one more time: put a ceiling on the write phase so a client that refuses to drain its buffer eventually loses the connection instead of holding it for the protocol-maximum two-minute zero-window dance.

The cleanest defense, though, is architectural, and it is why most internet-facing sites simply do not worry about Slowloris anymore. Put a buffering reverse proxy in front of the origin. A proxy like nginx, or a CDN edge, terminates the client’s TCP connection itself and does not forward the request to the origin until the request is complete. A Slowloris connection that never finishes its headers never produces a complete request, so the proxy never opens an origin connection on its behalf, and the slow connection is held at the event-driven edge where it costs almost nothing. This is exactly how a CDN absorbs the attack: Cloudflare, for instance, buffers the full request at its edge and times out incomplete ones after a series of keep-alive probes, so most Slowloris attempts against a proxied site die at the edge and never touch the origin worker pool. The mechanics of that edge absorption, anycast dispersion and scrubbing, are covered in the CDN volumetric absorption writeup, and the connection-count side, per-source caps and rate floors, is the territory of rate-limiting algorithms.

There is a sharp caveat on the proxy defense, and it is the failure mode worth remembering. The proxy only protects you if traffic actually has to go through it. If the origin’s real IP is reachable directly, because DNS leaks it, an unproxied subdomain exposes it, or a firewall rule allows direct connections, an attacker can connect to the origin and skip the buffering edge entirely. The slow attack then lands on the bare origin worker pool exactly as it would have in 2009. The defense is a path, and a path can be gone around.

Closing: the attack that ages well

Slowloris is old. It was published in 2009, the year before the first iPad, and the version number on the original tool barely moved. What is striking is how little the underlying mechanism has had to change. The attack works because HTTP is willing to wait for a slow client and because a server has a finite number of slots to wait in, and neither of those facts is going away. We patched the servers, shipped mod_reqtimeout, moved the default web tier toward event loops, and put buffering proxies in front of nearly everything. Those changes did not make the attack incorrect. They made the conditions for it rarer. The mechanism is exactly as sound as it ever was; it just has fewer places left to land.

That is the durable lesson hiding in this family. Bandwidth attacks get answered with more bandwidth, and the numbers keep climbing into the terabits because that is an arms race over capacity. Slow attacks are not a capacity problem, so you cannot buy your way out of them. They are an accounting problem: a server is keeping a resource reserved on the assumption that a connection making any progress at all is a connection worth waiting for. Every defense that works is a correction to that accounting, a rate floor, a hard ceiling, a buffering layer that refuses to reserve anything until the request is whole. The day someone ships a popular server that assigns a blocking thread per connection and waits patiently on idle reads, by default, with no rate floor, Slowloris will work against it on day one. The patience is the vulnerability, and patience is a feature nobody is going to remove.


Sources & further reading

  • Wikipedia (2009–2026), Slowloris (computer security) — origin (RSnake, 17 June 2009), mechanism, affected and resistant servers, and the Apache mitigation modules including mod_reqtimeout.
  • Wikipedia (2026), R-U-Dead-Yet — the slow-POST tool, its byte-at-a-time body delivery and ten-second intervals, and the “low and slow” classification.
  • Sergey Shekyan / Qualys (2022 rev.), Slow Read DoS attack — the canonical slow-read writeup: a 28-byte advertised window, zero-window probing, and the send-buffer requirement.
  • Sergey Shekyan / Qualys (2011), New open-source tool for slow HTTP attack vulnerabilitiesslowhttptest’s release, its coverage of slow headers/body/read, and the predecessors it credits.
  • shekyan/slowhttptest wiki (2012–2024), SlowReadTest — how the tester implements slow read via SO_RCVBUF, window-range options, and the pipeline factor.
  • Apache HTTP Server docs (current), mod_reqtimeout — the RequestReadTimeout directive, the default header=20-40,MinRate=500 body=20,MinRate=500, and version history.
  • Cloudflare (2024), Slowloris DDoS attack — vendor explanation of the mechanism and edge buffering as mitigation.
  • Cloudflare (2024), Low and slow attack — the category definition and why these attacks evade volumetric detection.
  • Apache HTTP Server docs (2.5/trunk), Apache MPM event — how the event MPM frees a worker thread while a connection waits, closing much of the classic gap.
  • NETSCOUT (2024), What is a slow read DDoS attack? — concise vendor description of reading the response slowly to exhaust the connection pool.
  • Trustwave SpiderLabs (rev.), Mitigation of slow read denial of service attack — ModSecurity-level detection and mitigation for the response-path variant.
  • Invicti (rev.), R.U.D.Y. attack — the slow-POST tool’s form-walking behavior and Content-Length abuse, with defensive notes.

Further reading