Programming

So you want to build a DDoS protection system, huh?

Started by leeeeeek · Feb 25, 2026

#11764
Submitted for the 2026 tforum writing competition, see /post/e74e0d2fa9a165ec3b43

People in the Internet drug community don't seem to understand even the very basics of network scheduling theory. And I don't blame them. Not because they are high on drugs, but because the resources out there are so unclear you wish you were. Don't worry, I will try to hash things out.

All DDoS protection systems try to solve the same problem: with limited resources, how do I please everyone? The answer to that is fair queuing algorithms.

Resource contention under load

Let's think of the dumb approach: no queuing or access control, complete free-for-all. Once the DDoS attack (or hug of death) comes, everyone can use an infinite amount of resources and the costs are shared over everyone. What's needed is an approach that limits the maximum resource usage and guarantees everyone some minimum allocation.

It's worth noting that for fair queuing to be relevant, there HAS TO BE A QUEUE. If your server is not under 100% load, you can just serve everyone! This is a good reminder for all the third-rate markets out there who insist on putting you through a gauntlet of captchas, despite there frankly not being anything of value to DDoS there. (In fact, for many of them it would be to do the world a favour.)

GPS

The "ideal" scheduling algorithm is known as generalized processor sharing (GPS). Each "flow" (client) has a "weight" (bandwidth allocation - like a priority). Iff load goes to 100%, the speed (as a percentage of the total link capacity) is limited to the weight divided by the total weights of all active clients -- if he has 3.00 weight and the other clients have 47.00 in total, he can use at most 3 / (47 + 3) = 6% of the link in that case. (Mathematically, `connection.rate = total_capacity * connection.weight / sum({connection.weight | active(connection)})`)

The problem with this "algorithm" is that the Internet is packet-switched. It can serve as a goal, but it cannot tell us how to achieve this result in a packet-switched network where we can't really send a packet byte by byte, but rather have to work in MTU-sized chunks.

WFQ

The best approximation that we have to GPS is weighted fair queueing (WFQ). In a nutshell: we calculate when the packets hypothetically *would* finish in GPS, and then we run them in that order.

WFQ looks like this:

Assume there is 100% load and we started sending a packet at time `t_0` while not using more capacity than we have.

If we are already sending a packet, we have to wait until that one is done (`t_0 = finish_time(packets[-1])`). Else, we can start sending the packet immediately (`t_0 = now()`). Regardless, it will finish sending at `t = t_0 + finish_time(packet)`, where `finish_time(packet) = packet.size / connection.rate`.

Now, here's the trick: Instead of seeing time as a fixed rate, we normalize it by load. If the load is twice as high, then "virtual time" progresses half as fast. Mathematically, we say that `V'(t) = 1/sum({connection.weight | active(connection)})`

Intuitively, this is telling us something: while a packet may take the same amount of resources regardless of when it's sent, it's in a way still more costly if processed when at high load.

But it is also mathematically convenient: rather than saying that our upload speed doubles if half as many people are on, we pretend that the upload speed is constant and that the time dilates and contracts.

This framing also just so happens to match GPS: If the load is twice as high, everything is twice as slow to run. As long as we are working with discrete (atomic, indivisible) packets this can be hard to understand, but if you think about it like fluids in pipes it can be more intuitive: if some inlet feeds a pipe a constant rate, and we add twice as many outlets, then each outlet would (in percentage terms) drain half as fast.

You now know what you need in order to build a WFQ scheduler: For each connection, check if there is a packet in flight. If there isn't, the start time is the current "virtual time"; if there is, the "virtual time" when it is expected to finish (had it been sent slowly, like in the continuous model) will be the start time. Then calculate how long our request would take *in virtual time* -- meaning the transmission rate is constant -- and we get a (virtual) finish time.

The actual scheduling rule is really simple: the sooner[1] the time, the higher priority; and since it's not a problem to send packets too early, you can just always serve the packet with the soonest arrival time!

The devil, however, is in the details. There is a small problem: Converting real time to virtual time is hard. We know the rate of change, `V'(t)` (slope, derivative) at each moment in time, but this changes each time a packet comes in or leaves. Integrating this to get `V(t)` is a hassle. If our DDoS filter is too slow, it is in itself vulnerable to DDoS. This is the opposite of what we want!

Hence, we need tricks.

SCFQ

The most obvious trick is known as Self-clocked fair queueing (SCFQ). If the problem is an integration problem, we can use an integration solution -- sample V'(t) at a lower resolution: assume that the total load never changes during the lifetime of a request, and then we can actually skip the integral:

Assume that a packet begins sending at some "virtual time" (at earliest now), takes a certain amount of "virtual time" to complete, and then is done. The real virtual time can never be before the start time (or it would not have been sent yet -- the start time is calculated as `max(V(t), ...)`). And if we know exactly when it was sent and exactly how many seconds it took, neither can the real virtual time be after the end time.

Hence, after the packet is processed, we may update: `V(t) = packet.start + finish_time(packet) = packet.start + (packet.size / packet.bandwidth)`. This way, we don't have to do anything at all to track the integral.

DRR

Another option is known as Deficit Round Robin (DRR). It does away with virtual time entirely.

Each connection gets a queue, a "deficit counter", and a "quantum" (priority). Each queue contains packets with sizes. Each tick, you loop over all the connections. you increment the counter, and if the counter exceeds weight (size/quantum) of the packet at the head of the queue, the packet is sent out. The weight is then subtracted from the deficit counter. Repeat until the queue is empty or the weight is too small to send anything, then continue to the next node.

Another, faster way to implement this would be:

Keep a list of queues and timestamps (one per node) . Each tick, you increment a local variable i. If any node now has a timestamp lower than i, send out the packet at the head of the queue and increment the timestamp by the weight of the new head packet.

[1]: An attentive reader might ask why we don't order by departure time rather than arrival time.

Conclusion

There is no conclusion. The existing resources on this are really bad and DDoS protection is too often in the hands of amateurs; even with a small bit of thought there is much room for improvements. You really need to start asking questions, the next time you are forced to solve eleven captchas to visit a site that barely has eleven users.
#11765
↳ Replying to @leeeeeek
There is at least one critical assumption in this post, the size of the queue. There are different types of DDoS attacks, like flooding or resource blocking and others.

For example a flooding attack will hit the TCP/IP listener so it start queuing connections in a rapid speed. The number of concurrent connections are limited by the o/s maximum number of file descriptors (Linux). The max number of file descriptors can be increased until the server has no more memory. So, in a situation like this the options for anti-DDoS are limited. More servers and adjusted bandwidth together with an aggressive connection timeout are simple counter measures. Other counter measures are layering where edge servers has to take the heat and filter out legit connections and timeout the DDoS connections.

Depending of DDoS attack type and where in the OSI stack it hits requires different counter measures. However, DDoS can't be fully eliminated, if the attacker has more resources, the DDoS will succeed.