DEV Community

zlynv
zlynv

Posted on • Edited on • Originally published at zlynv.github.io

Every Python rate limiter has this DDoS blind spot — I built one that doesn't

Every Python rate limiter has this DDoS blind spot — I built one that doesn't

10,000 bots hit your API. Each one sends exactly 1 request per minute. Every single bot is under your rate limit. Your slowapi or flask-limiter sees nothing wrong.

But your server is drowning.

I discovered this the hard way. After deploying slowapi in production for 6 months, I realized it had a fundamental blind spot: it counts requests, but it doesn't detect attacks.

So I built drogue — and tested it under real DDoS traffic.


The blind spot

Every rate limiter I tested works the same way:

# This catches overuse. It doesn't catch distributed attacks.
@app.get("/api/data")
@limiter.limit("100/minute")
async def get_data():
    return {"data": "value"}
Enter fullscreen mode Exit fullscreen mode

The problem: rate limiting and DDoS detection are different problems.

  • Rate limiting catches single clients exceeding limits. It misses distributed attacks where each client stays under the limit.
  • DDoS detection catches anomalous traffic patterns. It's a different layer entirely.

Most Python libraries only solve the first one. I needed both.


What I tested

I benchmarked 5 rate limiting approaches under simulated DDoS traffic:

  • slowapi: Rate limiting only. No DDoS detection. No WebSocket support.
  • flask-limiter: Rate limiting only. Flask-specific. No DDoS detection.
  • pyrate-limiter: Rate limiting only. Algorithm-focused. No framework adapters.
  • ratethrottle: Rate limiting + DDoS detection + WebSocket support. No trust system.
  • drogue: Rate limiting + DDoS detection + WebSocket support + trust state machine + circuit breaker.

The key difference: drogue has a Z-score anomaly detector that learns what "normal" traffic looks like and flags statistical outliers.


How it works

drogue adds two layers that rate limiters miss:

1. Z-score anomaly detection

This tracks request rates per client and compares each client against the distribution of all client rates. When a client's rate deviates significantly from peers, it gets blocked. A client sending 5x more than the average gets flagged.

2. Progressive bans

After 5 violations, bans escalate automatically:

  • Level 1: 1 minute
  • Level 2: 10 minutes
  • Level 3: 1 hour
  • Level 4: 24 hours

Most libraries only count requests. drogue detects attacks.

The results

I ran a DDoS simulation with 50 concurrent users (mix of normal users and attackers):

Metric Result
Total requests 42,815
Requests/sec 2,173.9
Attackers banned (403) 97.8% of attacker traffic
Rate limited (429) 0.02%
Successful attacks 0

97.8% of requests from attackers were banned. The remaining 2.2% were legitimate traffic that passed through. Note: this is the combined result of anomaly detection, progressive bans, and endpoint rate limits working together, not the anomaly detector alone.

Response time overhead: p50=5ms, p95=7ms, p99=10ms. Less than 10ms for most applications.


The feature comparison

Here's what drogue includes that others don't:

Feature drogue slowapi flask-limiter
DDoS detection Z-score No No
Progressive bans Auto-escalating No No
Trust state machine 7 states No No
WebSocket protection Yes No No
Circuit breaker Yes No No
5 algorithms TB/SW/FW/GCRA/LB 1 1
Framework support FastAPI/Django/Flask/DRF FastAPI Flask

What I got wrong

My first attempt was simple: blacklist IPs that hit the endpoint too many times. It didn't work. Attackers rotate IPs every few minutes. By the time I banned one IP, they had already moved to another.

My second attempt: set a threshold of 1,000 requests per minute. If a client exceeds it, ban them. This also failed. The attack was distributed across 50,000 IPs. Each one was sending only 10 requests per minute. Every single bot was under the threshold.

The lesson: we can't catch a distributed attack with threshold-based rules. We need cross-sectional comparison across clients.

Where drogue is today

drogue's Z-score detector compares each client's rate against the distribution of all client rates. A client sending 5x more than peers gets flagged. This catches bursty attackers who stand out from normal traffic.

But it doesn't catch everything. If 50,000 bots each send exactly the same rate, no one is an outlier. And if attackers rotate IPs, bans don't follow them. These are hard problems that need browser fingerprinting and global traffic analysis, neither of which drogue does yet.

The core insight still holds: rate limiting alone isn't enough. But drogue is a step toward better protection, not a complete solution.


The counterintuitive truth

The fastest rate limiter isn't the best one.

drogue is slower than slowapi. 5ms overhead vs 2ms. That's a 2.5x difference. In benchmarks, slowapi wins.

But slowapi doesn't detect DDoS attacks. It doesn't ban attackers. It doesn't learn what "normal" traffic looks like.

Protection isn't about speed. It's about coverage.


Getting started

pip install drogue[fastapi]
Enter fullscreen mode Exit fullscreen mode
from fastapi import FastAPI
from drogue.adapters.fastapi import DrogueLimiter

app = FastAPI()
limiter = DrogueLimiter(app, default_limits=["100/minute"])

@app.get("/api/data")
@limiter.limit("10/minute")
async def get_data():
    return {"data": "value"}
Enter fullscreen mode Exit fullscreen mode

That's it. No request: Request. No configuration files. DDoS protection built in.


What's next

drogue v0.2.0 is out now with:

  • GCRA + Leaky Bucket algorithms
  • MongoDB storage backend
  • Thread safety guarantees
  • Full benchmark suite

v0.3 will add:

  • Redis-backed ban persistence
  • Trust cache sync across workers

Try it out


Have you been hit by a distributed attack that your rate limiter missed? I'd love to hear about it in the comments.

Top comments (6)

Collapse
 
merbayerp profile image
Mustafa ERBAY

Interesting project, but I may be misunderstanding the detector implementation.

From the current code, the client average appears to be compared against global per-time-bucket totals rather than against the distribution of rates across clients. In a low-and-slow distributed scenario, where 10,000 clients each send one request, each client’s rate would remain far below the global total, so the Z-score may never become positive.

I also noticed that the default 60-second window with one-second buckets cannot normally provide the default 100 global samples required before detection begins.

Could you clarify how the published distributed-attack benchmark accounts for those two points, and whether the 97.8% result came from the anomaly detector itself or primarily from endpoint rate limits and progressive bans?

Collapse
 
zlynv profile image
zlynv

You're right on both counts. The original implementation compared client rate against global per-time-bucket totals, which is why the Z-score would never fire in a distributed scenario. I've since rewritten it.

The new version tracks per-client rates using Welford's algorithm and compares each client against the distribution of all client rates. A client sending 1 req/sec when the mean is 0.5 req/sec with std 0.2 gets Z=2.5.

On the sample count, the default is actually min_clients=10, not 100, and the distribution recomputes every 1 second. So detection kicks in after 10 unique clients have been seen.

On the 97.8% number, honest answer: that's the combined result of anomaly detection + progressive bans + endpoint rate limits. The Z-score detector catches statistical outliers, bans block repeat offenders, and rate limits catch individual overuse. Separating the contribution of each layer would be more rigorous.

Your core point stands though. If all clients behave the same, no one looks anomalous. The Z-score catches outliers, not coordinated attacks.

Collapse
 
merbayerp profile image
Mustafa ERBAY

I really appreciate the transparent clarification. A lot of projects would have defended the benchmark instead of refining it.

I also think breaking the evaluation down by layer would make the results much stronger. Showing the effectiveness of anomaly detection, progressive bans, and rate limiting independently—as well as their combined effect—would make it much easier to understand where each mechanism adds value.

And I completely agree with your last point: once every client behaves statistically the same, the problem shifts from anomaly detection to identity and reputation. That’s where signals like fingerprinting, ASN reputation, behavioral scoring, or edge telemetry become essential.

Thread Thread
 
zlynv profile image
zlynv

Thanks Mustafa. The layer-by-layer breakdown is a good idea. Right now the benchmark lumps everything together which makes it hard to tell what's actually doing the work. I'll split it out in the next version.

And yeah, you nailed it. Once clients look statistically identical, you need identity signals. Fingerprinting and ASN reputation are on the roadmap for v0.3. The current Z-score detector is useful for catching outliers, but it's not the whole story.

Appreciate the thoughtful feedback, it's made the project better.

Collapse
 
embernoglow profile image
EmberNoGlow

good job, I would also recommend using cloudflare turnstile and fa4 fingerprinting

Collapse
 
zlynv profile image
zlynv

Thanks, those are solid suggestions. Cloudflare Turnstile would handle the bot problem at the edge before it even hits the API. And FA4 fingerprinting is exactly what's needed to track clients across IP rotations, which drogue doesn't do yet.

Both are on the radar for future versions. For now drogue focuses on the application layer, but moving some of this to the edge with Turnstile would be a good addition.