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"}
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]
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"}
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
- GitHub: github.com/zlynv/drogue
- PyPI: pypi.org/project/drogue
- Docs: zlynv.github.io/drogue
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)
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?
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.
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.
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.
good job, I would also recommend using cloudflare turnstile and fa4 fingerprinting
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.