Back to Blog Search

    Attack Anatomy

    Why Rate Limiting Alone Can't Stop Modern DDoS Attacks

    Christopher Plunk

    Chief Technology Officer

    C
    July 13, 20267 min read

    There is a kind of DDoS that goes straight through SYN cookies, SYNPROXY, and every rate
    limit you have set, and it does that while looking exactly like your real customers. If
    your plan for an attack is a large enough rate limit, this is the one that takes you down
    anyway.

    We deal with it in production. This post covers what it is, why rate limiting can't fix
    it no matter how you tune it, and the approach that does.

    Two floods that a defender sees completely differently

    Most DDoS defenses were built for one kind of flood. The trouble is there are two, and
    they break on opposite sides.

    The spoofed volumetric flood

    The classic SYN flood forges its source addresses. It sends millions of connection
    requests from IPs it does not own, and it never plans to finish the conversation.

    This one is handled. The defense is stateless challenge and response:

    • SYN cookies encode the connection state inside the sequence number, so the server holds
      no half-open state. A spoofed source never gets the reply, so it never finishes the
      handshake and is dropped for free.

    • SYNPROXY does the same job at the edge, in the kernel, before the packet reaches your
      application.

      A forged source cannot answer the challenge, because the answer goes to the address it
      faked. Every serious provider handles this. If a vendor's pitch ends here, they are
      solving a problem from 2010.

    The non-spoofed distributed flood

    Now the attacker uses a real botnet: thousands, sometimes hundreds of thousands, of
    compromised machines, each with a real IP and a working network stack.

    Every bot opens a real TCP connection. It finishes the three-way handshake. It speaks
    valid protocol. Then it holds the slot, or it tears down and reconnects from a fresh
    address, again and again.

    Here is the problem. At layer 4 this looks the same as a cold legitimate connection. A
    first-time customer reaching your game server, your API, or your SSH bastion produces the
    same wire signature: a real SYN, a real handshake, valid data. SYN cookies pass it,
    because the handshake is real. SYNPROXY passes it for the same reason. A challenge-based
    defense has nothing to challenge.

    So you reach for a rate limit, and the numbers stop working in your favor.

    The math of a shared rate limit

    Protect a service with a rate limit of any kind: a token bucket, a connections-per-second
    cap, whatever you like. Legitimate traffic and attack traffic pull from the same budget,
    because you cannot tell them apart. That was the whole problem.

    If your cap admits C connections per second, legitimate demand is L, and the flood is F,
    your real users get this share of the admits:

    legit admitted  =  C x  L / (L + F)
    

    Put real numbers in. Say L is 200 real users and F is 60,000 attacking connections per
    second. Your legitimate users get about 0.3 percent of the budget. The rest goes to the
    flood. Customers see timeouts and failed connections. The service is down in every way
    that matters.

    The obvious fix is to raise the cap. It does not help. A bigger C admits more flood in
    the same proportion as more legit traffic, so the ratio L / (L + F) does not move. All
    you have done is deliver more of the attack to the origin you were trying to protect. At
    any finite cap a shared bucket hands the minority, your real users, a share of
    L / (L + F), and a large enough flood drives that toward zero. No value of the limit
    fixes it. The starvation is structural, not a tuning problem.

    This is what goes wrong during an incident. The team watches legit users get starved,
    cranks every limit up, and only ships more attack traffic to the thing under attack.

    What actually works: telling them apart

    If you cannot out-tune a shared bucket, the only way out is to stop sharing it between
    attacker and customer. That means separating them, and since they are identical for one
    packet, you have to judge them over time and by behavior rather than on any single packet.

    The approach that holds up against this class comes down to one shift: judge a source by
    how it behaves over time, not by any single packet.

    • Behavioral admission. A real client and a flood bot are identical for one packet, but
      they diverge across a full exchange. We make a source earn its slot by behaving the way a
      genuine client does, which thins the flood out before it ever competes with real users for
      capacity. The survivors are no longer up against the full attack.

    • Earned trust for proven-good sources. Once a connection is genuinely in use, it
      produces signals a flood cannot cheaply reproduce. We use those to fast-track sources that
      have already proven themselves, so returning legitimate users are not stuck behind the gate
      on every connect.

    • Reputation that only subtracts. Sources that clearly misbehave get pulled out of the
      shared budget. We only ever act on proof a source generates about itself, never on how much
      traffic it sends, so an attacker cannot fake the signal to get your real users blocked.

      Two things are not on that list: a bigger rate limit and a cleverer signature. A signature
      that blocks packets matching some pattern fails the moment the attacker changes the pattern,
      and against a non-spoofed flood there is nothing malformed to match on to begin with. The
      durable answer is behavioral, and it has to run somewhere it can act on millions of packets
      per second without falling over.

    Where the filtering runs decides whether it works

    All of this is pointless if the filtering sits too deep in the stack. By the time a flood
    reaches your application, or even your host TCP stack, you have already paid for it in
    CPU, memory, and connection-table pressure.

    Sterile filters at the NIC driver layer with XDP and eBPF, before the packet reaches the
    kernel TCP stack. Clean traffic is tunneled to your origin. Attack traffic is dropped in
    the driver, at line rate, on commodity hardware. Everything described above runs right
    there, at the edge, before your infrastructure ever sees the flood. Your origin only ever
    meets traffic that already earned its way through.

    What this means for you

    Game server operators (FiveM, Minecraft, Rust). You are the main target for this exact
    attack. Non-spoofed floods from booter and stresser botnets complete handshakes and bury
    real players. A provider whose story ends at "we have SYN cookies" will not save your
    server. Ask how they handle non-spoofed, handshake-completing floods, specifically.

    Hosting providers and ISPs. You are selling uptime. The math above is what turns "we
    rate limit at the edge" into a wall of support tickets during an incident. Behavioral
    admission at the edge is what keeps your customers' cold connections alive while the attack
    runs.

    Enterprise and SaaS. Your SLA depends on cold-connection success under load: the new
    customer, the failover, the health check. That is exactly what a shared rate limit starves
    first. The number that matters is not how much you dropped, it is whether first-time
    connections still got through.

    Frequently asked questions

    Is a big enough rate limit eventually enough?
    No. A bigger cap admits more attack in the same proportion as legit traffic. The share your
    users get is set by the ratio of legit to attack volume, not by the cap, and a larger flood
    drives it toward zero.

    Do SYN cookies stop this?
    They stop spoofed floods, where the source cannot complete the handshake. A non-spoofed
    distributed flood completes a real handshake, so SYN cookies and SYNPROXY pass it. There is
    nothing for them to challenge.

    Can you just block the attacking IPs?
    There are often hundreds of thousands of them, spread about one or two per /24 subnet, so
    prefix-level blocking either misses them or catches real users in the same subnets. The set
    also rotates constantly. Static blocklists lose that race.

    So what stops it?
    Behavioral discrimination at the edge: making a source prove it behaves like a real client
    before it earns a slot, fast-tracking sources that have already proven themselves, and
    removing the ones that clearly misbehave, all in the NIC driver before the flood reaches your
    origin.


    This is the class of attack Sterile is built to stop. See how it
    works
    or get in touch.

    Stay online. Stay Sterile.

    Related Posts