<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mustapha Nurudeen</title>
    <description>The latest articles on DEV Community by Mustapha Nurudeen (@mustapha_nurudeen_e1b7842).</description>
    <link>https://dev.to/mustapha_nurudeen_e1b7842</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3904075%2Fabb26a98-fa28-4cba-abc4-e961f08578a9.png</url>
      <title>DEV Community: Mustapha Nurudeen</title>
      <link>https://dev.to/mustapha_nurudeen_e1b7842</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mustapha_nurudeen_e1b7842"/>
    <language>en</language>
    <item>
      <title>How I Built a Real-Time DDoS Detection Engine from Scratch</title>
      <dc:creator>Mustapha Nurudeen</dc:creator>
      <pubDate>Wed, 29 Apr 2026 10:18:26 +0000</pubDate>
      <link>https://dev.to/mustapha_nurudeen_e1b7842/how-i-built-a-real-time-ddos-detection-engine-from-scratch-2180</link>
      <guid>https://dev.to/mustapha_nurudeen_e1b7842/how-i-built-a-real-time-ddos-detection-engine-from-scratch-2180</guid>
      <description>&lt;h1&gt;
  
  
  How I Built a Real-Time DDoS Detection Engine from Scratch
&lt;/h1&gt;

&lt;p&gt;If you have ever wondered how services detect and block DDoS attacks in real time,&lt;br&gt;
this post breaks it down using a project I built for the HNG DevSecOps programme.&lt;br&gt;
No fancy libraries. No Fail2Ban. Just Python, math, and iptables.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the Project Does
&lt;/h2&gt;

&lt;p&gt;I built a daemon that runs alongside a Nextcloud server and watches every single&lt;br&gt;
HTTP request coming in through Nginx. When it detects unusual traffic from a single&lt;br&gt;
IP or a global spike, it automatically blocks the attacker using iptables and sends&lt;br&gt;
a Slack alert within 10 seconds.&lt;/p&gt;

&lt;p&gt;Here is what it does continuously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reads Nginx access logs line by line in real time&lt;/li&gt;
&lt;li&gt;Tracks request rates using sliding windows&lt;/li&gt;
&lt;li&gt;Learns what normal traffic looks like using a rolling baseline&lt;/li&gt;
&lt;li&gt;Detects attacks using statistical anomaly detection&lt;/li&gt;
&lt;li&gt;Blocks attackers at the kernel level using iptables&lt;/li&gt;
&lt;li&gt;Automatically unbans IPs on a backoff schedule&lt;/li&gt;
&lt;li&gt;Shows everything on a live dashboard&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How the Sliding Window Works
&lt;/h2&gt;

&lt;p&gt;Imagine you want to know how many cars passed a checkpoint in the last 60 seconds.&lt;br&gt;
You could count every car that passed and reset every minute, but that gives you a&lt;br&gt;
per-minute count, not a true rolling 60-second window.&lt;/p&gt;

&lt;p&gt;The correct approach is to store the timestamp of every car that passed and evict&lt;br&gt;
old timestamps as time moves forward. That is exactly what I did using Python's&lt;br&gt;
built-in &lt;code&gt;collections.deque&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For every IP address, I maintain a deque of timestamps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;ip_windows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;  &lt;span class="c1"&gt;# one deque per IP
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ip_windows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;ip_windows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ip_windows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;evict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip_windows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popleft&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip_windows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;evict&lt;/code&gt; function pops timestamps from the left of the deque while they are&lt;br&gt;
older than 60 seconds. This gives us a true sliding window with no approximations.&lt;br&gt;
I also maintain a global window the same way to track overall traffic rate.&lt;/p&gt;
&lt;h2&gt;
  
  
  How the Baseline Learns From Traffic
&lt;/h2&gt;

&lt;p&gt;A static threshold like "block anyone sending more than 100 requests per minute"&lt;br&gt;
fails in production. Traffic patterns change by hour, by day, by event.&lt;/p&gt;

&lt;p&gt;Instead, I built a rolling baseline that learns from real traffic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every second, I record how many requests came in that second&lt;/li&gt;
&lt;li&gt;I store these counts in a rolling 30-minute window&lt;/li&gt;
&lt;li&gt;Every 60 seconds, I recalculate the mean and standard deviation of those counts&lt;/li&gt;
&lt;li&gt;I also track per-hour slots so the baseline reflects the current hour's pattern&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means at 3am when traffic is low, the baseline is low. At 2pm when traffic&lt;br&gt;
is high, the baseline adjusts upward. The system always compares current traffic&lt;br&gt;
against recent normal traffic, not a hardcoded number.&lt;/p&gt;
&lt;h2&gt;
  
  
  How the Detection Logic Makes a Decision
&lt;/h2&gt;

&lt;p&gt;Once I have the current rate and the baseline mean and standard deviation, I use&lt;br&gt;
two checks. Whichever fires first triggers the response:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check 1: Z-score&lt;/strong&gt;&lt;br&gt;
z = (current_rate - baseline_mean) / baseline_stddev&lt;br&gt;
if z &amp;gt; 3.0:&lt;br&gt;
flag as anomalous&lt;/p&gt;

&lt;p&gt;A z-score above 3.0 means the current rate is more than 3 standard deviations&lt;br&gt;
above normal. Statistically, this happens by chance less than 0.3% of the time&lt;br&gt;
in normal traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check 2: Rate multiplier&lt;/strong&gt;&lt;br&gt;
if current_rate &amp;gt; baseline_mean * 5.0:&lt;br&gt;
flag as anomalous&lt;/p&gt;

&lt;p&gt;This catches cases where the baseline stddev is very small and the z-score math&lt;br&gt;
might be slow to react.&lt;/p&gt;

&lt;p&gt;For example, if normal traffic is 2 requests per second and someone sends 50&lt;br&gt;
requests per second, the z-score would be enormous and the 5x check would also&lt;br&gt;
fire. The IP gets blocked.&lt;/p&gt;
&lt;h2&gt;
  
  
  How iptables Blocks an IP
&lt;/h2&gt;

&lt;p&gt;iptables is Linux's built-in firewall. It works at the kernel level, meaning&lt;br&gt;
blocked packets never even reach Nginx or Nextcloud. They are dropped before&lt;br&gt;
any application code runs.&lt;/p&gt;

&lt;p&gt;When my detector flags an IP, it runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iptables &lt;span class="nt"&gt;-I&lt;/span&gt; INPUT &lt;span class="nt"&gt;-s&lt;/span&gt; &amp;lt;attacker_ip&amp;gt; &lt;span class="nt"&gt;-j&lt;/span&gt; DROP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This inserts a rule at the top of the INPUT chain that silently drops all packets&lt;br&gt;
from that IP. The attacker gets no response, no error, nothing.&lt;/p&gt;

&lt;p&gt;To unban after the timeout expires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iptables &lt;span class="nt"&gt;-D&lt;/span&gt; INPUT &lt;span class="nt"&gt;-s&lt;/span&gt; &amp;lt;attacker_ip&amp;gt; &lt;span class="nt"&gt;-j&lt;/span&gt; DROP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ban durations follow a backoff schedule: 10 minutes for the first offense,&lt;br&gt;
30 minutes for the second, 2 hours for the third, then permanent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;When I simulated a DDoS attack by sending 500 rapid requests to the server, the&lt;br&gt;
detector responded within seconds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global anomaly alert fired (zscore=3.03)&lt;/li&gt;
&lt;li&gt;Per-IP ban triggered within 2 seconds&lt;/li&gt;
&lt;li&gt;Slack notification sent&lt;/li&gt;
&lt;li&gt;iptables DROP rule added&lt;/li&gt;
&lt;li&gt;After 10 minutes, automatic unban with Slack notification&lt;/li&gt;
&lt;li&gt;Baseline recalculated to reflect the new traffic pattern&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The live dashboard at dashboard.mustaphaolalekan.online:5000 shows all of this&lt;br&gt;
in real time, refreshing every 3 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sliding windows with deques give you true rolling rate calculations&lt;/li&gt;
&lt;li&gt;Statistical baselines adapt to real traffic patterns automatically&lt;/li&gt;
&lt;li&gt;iptables blocks at the kernel level before your app even sees the packet&lt;/li&gt;
&lt;li&gt;A backoff unban schedule is gentler than permanent bans for first offenders&lt;/li&gt;
&lt;li&gt;Everything should be in a config file, never hardcoded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full source code is at:&lt;br&gt;
&lt;a href="https://github.com/MustaphaCloud/hng-ddos-detector" rel="noopener noreferrer"&gt;https://github.com/MustaphaCloud/hng-ddos-detector&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>python</category>
      <category>networking</category>
    </item>
  </channel>
</rss>
