How I Built a Real-Time DDoS Detection Engine from Scratch
If you have ever wondered how services detect and block DDoS attacks in real time,
this post breaks it down using a project I built for the HNG DevSecOps programme.
No fancy libraries. No Fail2Ban. Just Python, math, and iptables.
What the Project Does
I built a daemon that runs alongside a Nextcloud server and watches every single
HTTP request coming in through Nginx. When it detects unusual traffic from a single
IP or a global spike, it automatically blocks the attacker using iptables and sends
a Slack alert within 10 seconds.
Here is what it does continuously:
- Reads Nginx access logs line by line in real time
- Tracks request rates using sliding windows
- Learns what normal traffic looks like using a rolling baseline
- Detects attacks using statistical anomaly detection
- Blocks attackers at the kernel level using iptables
- Automatically unbans IPs on a backoff schedule
- Shows everything on a live dashboard
How the Sliding Window Works
Imagine you want to know how many cars passed a checkpoint in the last 60 seconds.
You could count every car that passed and reset every minute, but that gives you a
per-minute count, not a true rolling 60-second window.
The correct approach is to store the timestamp of every car that passed and evict
old timestamps as time moves forward. That is exactly what I did using Python's
built-in collections.deque.
For every IP address, I maintain a deque of timestamps:
from collections import deque
import time
ip_windows = {} # one deque per IP
def record(ip):
now = time.time()
if ip not in ip_windows:
ip_windows[ip] = deque()
ip_windows[ip].append(now)
evict(ip_windows[ip], now)
def evict(window, now):
cutoff = now - 60
while window and window[0] < cutoff:
window.popleft()
def get_rate(ip):
return len(ip_windows.get(ip, [])) / 60
The evict function pops timestamps from the left of the deque while they are
older than 60 seconds. This gives us a true sliding window with no approximations.
I also maintain a global window the same way to track overall traffic rate.
How the Baseline Learns From Traffic
A static threshold like "block anyone sending more than 100 requests per minute"
fails in production. Traffic patterns change by hour, by day, by event.
Instead, I built a rolling baseline that learns from real traffic:
- Every second, I record how many requests came in that second
- I store these counts in a rolling 30-minute window
- Every 60 seconds, I recalculate the mean and standard deviation of those counts
- I also track per-hour slots so the baseline reflects the current hour's pattern
This means at 3am when traffic is low, the baseline is low. At 2pm when traffic
is high, the baseline adjusts upward. The system always compares current traffic
against recent normal traffic, not a hardcoded number.
How the Detection Logic Makes a Decision
Once I have the current rate and the baseline mean and standard deviation, I use
two checks. Whichever fires first triggers the response:
Check 1: Z-score
z = (current_rate - baseline_mean) / baseline_stddev
if z > 3.0:
flag as anomalous
A z-score above 3.0 means the current rate is more than 3 standard deviations
above normal. Statistically, this happens by chance less than 0.3% of the time
in normal traffic.
Check 2: Rate multiplier
if current_rate > baseline_mean * 5.0:
flag as anomalous
This catches cases where the baseline stddev is very small and the z-score math
might be slow to react.
For example, if normal traffic is 2 requests per second and someone sends 50
requests per second, the z-score would be enormous and the 5x check would also
fire. The IP gets blocked.
How iptables Blocks an IP
iptables is Linux's built-in firewall. It works at the kernel level, meaning
blocked packets never even reach Nginx or Nextcloud. They are dropped before
any application code runs.
When my detector flags an IP, it runs:
iptables -I INPUT -s <attacker_ip> -j DROP
This inserts a rule at the top of the INPUT chain that silently drops all packets
from that IP. The attacker gets no response, no error, nothing.
To unban after the timeout expires:
iptables -D INPUT -s <attacker_ip> -j DROP
The ban durations follow a backoff schedule: 10 minutes for the first offense,
30 minutes for the second, 2 hours for the third, then permanent.
The Result
When I simulated a DDoS attack by sending 500 rapid requests to the server, the
detector responded within seconds:
- Global anomaly alert fired (zscore=3.03)
- Per-IP ban triggered within 2 seconds
- Slack notification sent
- iptables DROP rule added
- After 10 minutes, automatic unban with Slack notification
- Baseline recalculated to reflect the new traffic pattern
The live dashboard at dashboard.mustaphaolalekan.online:5000 shows all of this
in real time, refreshing every 3 seconds.
Key Takeaways
- Sliding windows with deques give you true rolling rate calculations
- Statistical baselines adapt to real traffic patterns automatically
- iptables blocks at the kernel level before your app even sees the packet
- A backoff unban schedule is gentler than permanent bans for first offenders
- Everything should be in a config file, never hardcoded
The full source code is at:
https://github.com/MustaphaCloud/hng-ddos-detector
Top comments (0)