RUDY Attack: How It Works and How to Test Your Defenses

A long ribbon fed one sparse glowing fragment at a time, illustrating how a RUDY attack trickles a request body

· 12 min read

A RUDY attack does not look like an attack. The request line is valid, the header block is finished, and the Content-Length tells the server exactly how many bytes are coming. That number is true. The only odd thing is how slowly the body arrives: one byte at a time, with a long pause between each one.

That is what makes it harder to catch than Slowloris. Slowloris sends a request it never finishes, so anything waiting for a finished request notices something is missing. RUDY finishes the part your controls inspect and starves the part they do not. Your WAF finds nothing wrong, because on the evidence available to it, nothing is wrong.

The short version

  • The request is valid, which is the whole problem. Complete headers, truthful Content-Length. A WAF sees a legitimate POST rather than an unfinished one, so inspection has nothing to flag.
  • One socket can pin a worker for days. A declared body of 100,000 bytes at one byte every ten seconds takes 1,000,000 seconds to finish, roughly 11.6 days.
  • An idle timeout does not stop it. nginx documents client_body_timeout (default 60s) as the limit between two successive reads, not on the body as a whole. A byte every ten seconds resets that clock.
  • The control that decides this is a minimum data rate on the body read. On Apache that is mod_reqtimeout, an extension module that does nothing unless it is loaded.
  • Watch held connections, not request rate. Each slot opens one socket and keeps it, so the rate graph reads near zero while the attack works.

This is one post in a series covering each of the ten simulation types Obsidio runs. They fail in genuinely different places, which is why a test programme needs coverage across the categories rather than volume in one of them. For how they fit together, 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.

Application-layer floods

Slow attacks

Connection exhaustion

Protocol-specific and scripted

What a RUDY attack actually does

RUDY, short for R-U-Dead-Yet, opens parallel HTTP POST connections and gives each one a complete request head: request line, Host, a content type, and a Content-Length stating the exact size of the body to follow. Then it delivers that body one byte at a time.

The server does the only reasonable thing. It knows how many bytes are coming, it has received some, and the client is plainly still there, so it keeps the request handler assigned and waits. That handler, a thread or a worker slot, serves nobody else meanwhile. Multiply by a few hundred connections and the pool is gone. Real users get a timeout, not an error page.

Think of twenty people filling out a long form at the counter, writing one letter every ten seconds. The clerk has to wait. They cannot move on until the form is done, and it is being done, just not this year.

The arithmetic explains why nothing recycles. The default declared Content-Length is 100,000 bytes and the default gap between bytes is 10 seconds, so the body needs 1,000,000 seconds to complete, about 11.6 days. No socket in a realistic run gets there. Each slot holds its socket until the server tears it down or the run ends, so at 20 sockets per worker the wedged-request count is roughly 20 multiplied by the worker count, and it stays there.

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. Waits for a traffic spike. A byte every ten seconds is not one.
  • Firewall and WAF. The hard part. Headers are complete and the declared length is honest, so inspection sees a valid POST and passes it. It helps only if it buffers the whole body before forwarding, and plenty of deployments stream instead.
  • Rate limiting. Counts requests. Each source sends a handful, then goes quiet.
  • Bot detection and challenges. No page is ever loaded, so there is nothing to challenge.
  • CDN cache. A POST is not cacheable, so the cache is not in the path.

Zone 2

What takes the hit?

  • Load balancer or reverse proxy. Absorbs the whole attack if it buffers request bodies, taking the connection cost itself. Worth confirming yours does.
  • Connection layer. Fills with sockets that are active, not idle.
  • Web server and application server. Where it bites. Handlers sit pinned waiting for a body unless a rate floor on the body read cuts them off.
  • Application and database. Never reached. The handler is stuck in front of them holding half a form.
  • Auto-scaling. Sees no load spike, so it never scales.

Zone 3

Would you find out?

  • Dashboards. Requests per second look completely normal.
  • A check from outside your network. The one thing that reliably catches this, if you run one.
  • Logging and your SIEM. A request reaches the access log only once it ends, and then it looks like a client on a poor connection that timed out.
  • Alerting. Nothing crosses a threshold.
  • On-call. Nobody is paged. The service is down and the room is quiet.

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.

Zone 1 holds the difference between this attack and Slowloris. A WAF that waits for a complete request has a defensible reason to hold Slowloris traffic: the request is unfinished. RUDY takes that reason away. A control asked to decide whether this request is legitimate will decide it is, and be right. The only signal left is how fast the body arrives, and most request-inspection products have no opinion about that.

What separates RUDY from Slowloris

Five differences matter. Each points at a different control.

  • Where the request is incomplete. Classic Slowloris withholds the blank line that ends the header block, so the server stays parked in “reading request headers”. RUDY sends that block correctly and withholds the body instead. A header-read timeout catches the first and has no bearing on the second.
  • Whether the end of the body is declared. Slowloris has a slow-body variant, but it uses chunked transfer encoding and never sends the terminating zero-length chunk, so the body has no declared end. RUDY declares an exact length and honours it, slowly. No terminator is missing.
  • What runs out first. Slowloris targets the connection pool in front of your application. RUDY targets request handlers, because the server has accepted the request and committed a worker to it.
  • Which timeout decides it. A header-read timeout settles Slowloris. A body-read timeout settles RUDY, and only if it enforces a minimum transfer rate.
  • How it looks in a log. An unfinished Slowloris request often produces no entry. A RUDY request looks like an ordinary POST from a client on a bad line.

There are only a handful of parameters. Sockets per worker defaults to 20 and runs from 1 to 100, setting how many handlers you are trying to pin. The gap between body bytes defaults to 10 seconds and ranges from 1 to 60: shorter recycles sockets more often, longer makes each one harder to time out but risks the server closing it for inactivity. The declared Content-Length defaults to 100,000 bytes and ranges from 1,000 to 10,000,000, fixing each socket’s lifetime alongside the interval.

Host, Content-Type and Content-Length are forced and cannot be overridden. The truthful declared length is the mechanism, so letting it be changed would make this a different simulation.

How to defend against a RUDY attack

What decides this is a rule about how slowly a client may deliver a body it already promised. Three controls do the work; two popular candidates do not.

  • A minimum data rate on the body read. On Apache this is mod_reqtimeout, whose documented default is RequestReadTimeout handshake=0 header=20-40,MinRate=500 body=20,MinRate=500. The body setting allows at least 20 seconds, and the documentation states that whenever data is received the timeout is increased according to the minimum data rate, so 500 bytes buys one more second. A byte every ten seconds buys one five-hundredth of a second, so the allowance barely moves and the connection closes. Two caveats: the module has extension status and is inactive unless loaded, and the default body value sets no upper bound, so a client that really sustains 500 bytes per second keeps extending its timeout.
  • Do not rely on an idle timeout. nginx ships client_body_timeout 60s, and the documentation is explicit that it covers only the period between two successive read operations, not the whole body. A byte every ten seconds resets it indefinitely, and since the attacker picks the interval, no idle timeout long enough for real users is short enough to help. The control has the wrong shape for this attack rather than the wrong value.
  • A reverse proxy that buffers request bodies. nginx defaults to proxy_request_buffering on, documented as reading the entire request body from the client before sending the request to the proxied server, so your upstream is never contacted until the body is complete. Strong protection on two conditions: the origin cannot be reached directly, and the proxy holds that many connections cheaply.
  • Per-source concurrent connection limits. Ends a single-source attempt at once, does far less against load spread over thousands of addresses.
  • Body size caps will not save you. Apache’s LimitRequestBody defaults to 1073741824 bytes in current 2.4 releases, and was 0, meaning unlimited, in 2.4.53 and earlier. nginx defaults client_max_body_size to 1m. A declared 100,000 bytes passes both, and a tighter cap only recycles sockets sooner.

What you can check yourself, and what it will get wrong

Some of this you can settle today without help. It is worth knowing which parts, and where a cheap check misleads you.

Read the configuration. Confirm whether a body-read timeout exists, whether it enforces a rate floor or only an idle gap, whether the module is loaded rather than merely available, and whether your proxy buffers bodies. That tells you the setting exists, not that it holds under concurrency.

Test alert routing on its own. Fire a synthetic alert and watch whether it reaches the right rotation and whether anybody acts. That verifies the plumbing and needs no attack traffic.

Run a single-source test against a staging environment, with change-management sign-off, on infrastructure you own. That is legitimate and useful, and it is also where the trap is.

A test from one machine does not simply fail to answer the Zone 1 questions. It answers several wrongly, in a reassuring direction. Your per-source connection limit catches one address holding twenty sockets almost immediately, and reputation filtering handles a lone unfamiliar client. Every control in the front zone appears to work, so you conclude you are covered.

Then a distributed version arrives from thousands of residential addresses holding a few sockets each, and those controls behave differently, because they key on source concentration rather than the behaviour doing the damage. The cheap check produces a false pass on the controls you most need to trust.

How Obsidio tests a RUDY attack

Whether your handler pool holds depends on how your proxy, web server, application server and connection limits interact under real concurrency, and that appears in no single config file. Obsidio runs RUDY 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 sockets per worker, the interval between bytes and the declared content length, then watch your stack. Because load comes from 100,000+ globally distributed real devices rather than a handful of datacenter instances, a per-source connection cap gets tested the way a distributed attack would test it.

Watch the right number. For RUDY that is active and peak held connections per bucket, because it counts how many handlers are wedged at any moment. The request-rate figure counts socket opens, and each slot opens one and keeps it, so the rate reads close to nothing. A team watching the rate graph will decide the test is not working while the handler pool empties underneath them.

Be clear about the division of evidence. Obsidio reports how many connections were held and how your edge responded, because both are visible from outside. Whether your SIEM recorded anything, whether a dashboard moved and whether anyone was paged live only on your own consoles, so have someone watching during the window.

Strength is proven, not promised. RUDY does not test your bandwidth. It tests whether anything in your stack has an opinion about how slowly a client is allowed to keep its promises.

Where this fits in a testing programme

RUDY and Slowloris are the two halves of the slow-attack category, and running one does not cover the other. Slowloris starves a header block and is answered by a header-read timeout. RUDY starves a declared body and is answered by a rate floor on the body read. Different settings, usually in different places, often owned by different teams. Neither is exercised by an application-layer flood test, so a programme built only on HTTP floods leaves this class unmeasured.

The ten simulation types fail in genuinely different places. An HTTP flood inverts nearly everything above: the front defenses finally have something to act on, the damage reaches your database connection pool, and the detection problem flips from hearing nothing to drowning in alerts. Coverage across the categories is the point, not volume in one of them.

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 a run leaves an artifact you can file rather than a screenshot you vouch for.

To scope an authorized RUDY simulation against your own infrastructure, see the Obsidio platform or get in touch. Independent, neutral, verifiable: resilience you can prove.

← Back to Blog