<?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: Luke David</title>
    <description>The latest articles on DEV Community by Luke David (@lucadavid075).</description>
    <link>https://dev.to/lucadavid075</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%2F462433%2Fe410e70c-fa3d-465f-9cad-8b5cf6799682.png</url>
      <title>DEV Community: Luke David</title>
      <link>https://dev.to/lucadavid075</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lucadavid075"/>
    <language>en</language>
    <item>
      <title>How I Built a Real-Time DDoS Detection Engine from Scratch</title>
      <dc:creator>Luke David</dc:creator>
      <pubDate>Wed, 29 Apr 2026 21:45:26 +0000</pubDate>
      <link>https://dev.to/lucadavid075/how-i-built-a-real-time-ddos-detection-engine-from-scratch-3p6f</link>
      <guid>https://dev.to/lucadavid075/how-i-built-a-real-time-ddos-detection-engine-from-scratch-3p6f</guid>
      <description>&lt;p&gt;If someone floods your web server with thousands of requests per second, what happens? Your server slows down, legitimate users can't get through, and eventually everything crashes. This kind of attack is called a &lt;strong&gt;DDoS&lt;/strong&gt; — a Distributed Denial of Service attack.&lt;/p&gt;

&lt;p&gt;In this post, I'll walk you through exactly how I built a tool that watches incoming traffic in real time, learns what "normal" looks like, detects when something is wrong, and automatically blocks the attacker — all in pure Python, without any rate-limiting libraries.&lt;/p&gt;

&lt;p&gt;By the end you'll understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How a sliding window works to measure request rates&lt;/li&gt;
&lt;li&gt;How a rolling baseline learns from your own traffic&lt;/li&gt;
&lt;li&gt;How the detection logic decides when something is suspicious&lt;/li&gt;
&lt;li&gt;How &lt;code&gt;iptables&lt;/code&gt; blocks an IP at the Linux kernel level&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's go.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are We Building?
&lt;/h2&gt;

&lt;p&gt;Imagine a security guard standing at the door of a nightclub. They don't just check IDs — they also watch how many people are arriving per minute. If suddenly 500 people show up in 60 seconds when the normal rate is 20 per minute, something is wrong.&lt;/p&gt;

&lt;p&gt;That's exactly what our tool does, but for HTTP requests to a web server.&lt;/p&gt;

&lt;p&gt;Here's the big picture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet
   │
   ▼
iptables (kernel firewall — blocks banned IPs before they reach Nginx)
   │
   ▼
Nginx (reverse proxy — logs every request as JSON)
   │
   ▼
Nextcloud (the actual cloud storage app)

Meanwhile, in parallel:
Nginx log ──► Python daemon reads it ──► Detects anomalies ──► Bans IPs via iptables
                                                             └──► Slack alert
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our Python daemon is always running in the background, reading the log, measuring rates, and reacting. It is &lt;strong&gt;not a cron job&lt;/strong&gt; — it is a continuous process with four threads running simultaneously, each with a specific job.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: The Sliding Window
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem with Simple Counters
&lt;/h3&gt;

&lt;p&gt;The most naive approach is a simple counter: "count requests per minute and reset every 60 seconds." But this has a well-known flaw called the &lt;strong&gt;boundary problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Imagine the window resets at 12:00:00. An attacker sends 99 requests at 11:59:59 and another 99 at 12:00:01. Your counter sees 99 in each window — fine! But in reality, 198 requests arrived in just 2 seconds. The counter completely missed it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Deque Solution
&lt;/h3&gt;

&lt;p&gt;We fix this with a &lt;strong&gt;sliding window&lt;/strong&gt; using Python's &lt;code&gt;collections.deque&lt;/code&gt;. Instead of counting in a fixed time slot, we store the exact Unix timestamp of every recent request and always look backwards exactly 60 seconds.&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="c1"&gt;# One deque per IP address — stores Unix timestamps
&lt;/span&gt;&lt;span class="n"&gt;per_ip_window&lt;/span&gt; &lt;span class="o"&gt;=&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;record_request&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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;per_ip_window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;per_ip_window&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;dq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;per_ip_window&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="c1"&gt;# Add this request's timestamp to the right
&lt;/span&gt;    &lt;span class="n"&gt;dq&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;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Evict timestamps older than 60 seconds from the left
&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;timestamp&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;dq&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dq&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;dq&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="c1"&gt;# O(1) — deque left-pop is constant time
&lt;/span&gt;
    &lt;span class="c1"&gt;# What remains = requests in the last 60 seconds
&lt;/span&gt;    &lt;span class="n"&gt;raw_count&lt;/span&gt;  &lt;span class="o"&gt;=&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;dq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rate_per_s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw_count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;   &lt;span class="c1"&gt;# convert to req/s to match baseline units
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rate_per_s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;deque&lt;/code&gt; instead of a regular list?&lt;/strong&gt; Because removing from the left of a list is O(n) — it shifts every element. A &lt;code&gt;deque&lt;/code&gt; removes from the left in O(1) constant time. Under a flood where we might evict thousands of entries per second, this difference is the gap between keeping up with the attack and falling behind.&lt;/p&gt;

&lt;p&gt;We maintain two of these structures simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-IP window&lt;/strong&gt; — tracks how fast one specific IP is sending requests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global window&lt;/strong&gt; — tracks total traffic from all IPs combined&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The global window catches distributed attacks where no single IP looks bad, but the total is overwhelming.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tracing Through an Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;12:00:01 — IP sends request. dq = [1.0]. rate = 1/60 = 0.017 req/s
12:00:30 — IP sends 10 more. dq = [1.0, ..., 30.0]. rate = 11/60 = 0.18 req/s
12:01:01 — cutoff = 61.0 - 60 = 1.0 → evict dq[0]=1.0
           dq = [2.0, ..., 61.0]. rate still ~0.18 req/s  ← normal

12:01:01 — Attacker sends 500 requests in 1 second
           dq now has 510 entries
           rate = 510/60 = 8.5 req/s  ← SUSPICIOUS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 2: The Rolling Baseline
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Not Hardcode a Threshold?
&lt;/h3&gt;

&lt;p&gt;You might think: "Just flag anyone over 100 requests per minute." But this fails immediately in the real world. A news article might suddenly make your site popular — totally normal traffic that would look like an attack. At 3am your server might handle 5 req/s normally; during business hours, 50 req/s is normal. A fixed threshold would either miss real attacks during busy periods or generate constant false positives during quiet ones.&lt;/p&gt;

&lt;p&gt;The solution is to &lt;strong&gt;learn from your own traffic&lt;/strong&gt; and adapt constantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rolling Mean and Standard Deviation
&lt;/h3&gt;

&lt;p&gt;Every second, we record how many requests arrived globally. We keep the last 30 minutes of these per-second counts in a deque (maxlen=1800). Every 60 seconds we recalculate:&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;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recalculate_baseline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&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;counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;   &lt;span class="c1"&gt;# not enough data — don't fire yet
&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt;      &lt;span class="o"&gt;=&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;counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mean&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="n"&gt;var&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="n"&gt;stddev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Proportional stddev floor — prevents near-zero variance
&lt;/span&gt;    &lt;span class="c1"&gt;# from causing absurd z-scores on perfectly uniform traffic.
&lt;/span&gt;    &lt;span class="c1"&gt;# We scale with mean so this is never a hardcoded constant.
&lt;/span&gt;    &lt;span class="c1"&gt;# Note: mean itself is NEVER modified — only stddev gets a floor.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;stddev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stddev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stddev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why a proportional stddev floor?&lt;/strong&gt; Consider a server getting exactly 1 request per second from background scanners. Every second has count=1. Mean=1.0, stddev=0.0. Now a user loads a page and sends 3 requests in one second — z-score = (3-1)/0.000001 = 2,000,000. They get banned for loading a web page.&lt;/p&gt;

&lt;p&gt;By enforcing &lt;code&gt;stddev &amp;gt;= mean * 0.3&lt;/code&gt;, we ensure z-scores stay meaningful even when traffic is perfectly uniform. The key point: &lt;strong&gt;we never modify the mean&lt;/strong&gt; — only the standard deviation gets a floor, and that floor scales proportionally with traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cold-Start Guard
&lt;/h3&gt;

&lt;p&gt;There is one more important detail: &lt;strong&gt;we do not fire any bans until the baseline has collected at least 120 per-second data points&lt;/strong&gt; (about 2 minutes of traffic). A brand-new baseline with 5 data points is not reliable enough to justify blocking anyone. This cold-start guard prevents false positives on startup without hardcoding any threshold values.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-Hour Slots — Preferring Recent Data
&lt;/h3&gt;

&lt;p&gt;Alongside the 30-minute window, we also maintain per-hour slots:&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="c1"&gt;# { "2026-04-27T14": [count_s1, count_s2, ...] }
&lt;/span&gt;&lt;span class="n"&gt;hourly_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="n"&gt;current_hour&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%dT%H&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&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;hourly_data&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;current_hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hourly_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;current_hour&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="c1"&gt;# prefer this hour's data
&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rolling_window&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# fall back to 30-min window
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why? If it's 3pm and you have 2 hours of 3pm data, it is more representative of current traffic than mixing in data from 1pm. The hourly preference kicks in after 120 seconds of same-hour data and gives the baseline a much more accurate picture of what is normal &lt;em&gt;right now&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;After the server has been running for several hours, you can see two hourly slots with visibly different mean values on the dashboard — proof the baseline is adapting to real traffic patterns.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: The Detection Logic
&lt;/h2&gt;

&lt;p&gt;Now we have two things: the current rate in req/s (from the sliding window) and what normal looks like in req/s (from the baseline). How do we decide if something is an attack?&lt;/p&gt;

&lt;h3&gt;
  
  
  Z-Score: How Many Standard Deviations Away Are We?
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;z-score&lt;/strong&gt; measures how far a value is from the mean, in units of standard deviations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;z-score = (current_rate - mean) / stddev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If traffic is normally 2.0 req/s with a stddev of 0.6, and an IP suddenly sends 10 req/s:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;z-score = (10.0 - 2.0) / 0.6 = 13.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A z-score of 13.3 means this rate would occur by random chance less than 0.000001% of the time under normal conditions. Something is very wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two Conditions — Whichever Fires First
&lt;/h3&gt;

&lt;p&gt;The spec requires us to flag an anomaly if &lt;strong&gt;either&lt;/strong&gt; condition triggers, whichever comes first:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_for_anomaly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate_per_s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stddev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Both rate_per_s and mean are in req/s — consistent units
&lt;/span&gt;    &lt;span class="n"&gt;zscore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate_per_s&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;stddev&lt;/span&gt;

    &lt;span class="c1"&gt;# Condition 1: statistically anomalous
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;zscore&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zscore=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;zscore&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;gt; 3.0 | rate=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rate_per_s&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; req/s | mean=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; req/s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# Condition 2: absolute spike — 5x the mean
&lt;/span&gt;    &lt;span class="c1"&gt;# Catches massive floods even when stddev is high
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rate_per_s&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spike: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rate_per_s&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; req/s &amp;gt; 5x mean=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; req/s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;   &lt;span class="c1"&gt;# all good
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The z-score catches statistically unusual traffic. The spike multiplier (5×) catches massive floods immediately even when variance is high. Both conditions use req/s throughout — no unit mixing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error Surge — Tightening Thresholds
&lt;/h3&gt;

&lt;p&gt;Attackers often probe for vulnerabilities by sending requests that return 404 or 403 errors. Our daemon tracks the error rate (fraction of 4xx/5xx responses) for each IP over the last 60 seconds, and compares it against the global baseline error rate:&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="c1"&gt;# Compare THIS IP's error rate against the GLOBAL baseline
&lt;/span&gt;&lt;span class="n"&gt;ip_error_rate&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;errors_from_this_ip&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total_from_this_ip&lt;/span&gt;
&lt;span class="n"&gt;global_error_rate&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total_errors_all_ips&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total_requests_all_ips&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ip_error_rate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;global_error_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# This IP is generating 3x more errors than normal
&lt;/span&gt;    &lt;span class="c1"&gt;# It's probably scanning/probing — watch it more closely
&lt;/span&gt;    &lt;span class="n"&gt;zscore_threshold&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;   &lt;span class="c1"&gt;# 3.0 → 1.5 (easier to flag)
&lt;/span&gt;    &lt;span class="n"&gt;spike_multiplier&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;   &lt;span class="c1"&gt;# 5.0 → 2.5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means a scanning IP gets flagged much sooner than a regular flooder, because it reveals its intent through error patterns before it even sends enough traffic to trigger the rate-based detection.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: Blocking with iptables
&lt;/h2&gt;

&lt;p&gt;When we confirm an anomaly from a specific IP, we block it immediately using &lt;strong&gt;iptables&lt;/strong&gt; — the Linux kernel's built-in firewall.&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;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ban_ip&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iptables&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-I&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INPUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# insert at the TOP of the INPUT chain
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-s&lt;/span&gt;&lt;span class="sh"&gt;"&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="c1"&gt;# from this source IP
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-j&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DROP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;          &lt;span class="c1"&gt;# silently drop all packets
&lt;/span&gt;    &lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why does this work so effectively?&lt;/strong&gt; iptables is enforced by the Linux kernel &lt;em&gt;before&lt;/em&gt; the packet reaches Nginx, before Docker, before anything. So a banned IP's requests are dropped at the network layer — your server wastes zero CPU processing them. The attacker gets no response at all; their connection just times out.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-I INPUT 1&lt;/code&gt; flag is important — it inserts the rule at position 1 (the very top of the chain) so it is checked first, before any other existing rules.&lt;/p&gt;

&lt;p&gt;To remove a ban:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;unban_ip&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iptables&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-D&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INPUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-s&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-j&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DROP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Backoff Schedule
&lt;/h3&gt;

&lt;p&gt;We don't ban permanently on the first offence. This is fair to legitimate users who might be behind a misconfigured corporate proxy sharing an IP. Instead, we use an escalating schedule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First offence:  ban 10 minutes
Second offence: ban 30 minutes
Third offence:  ban 2 hours
Fourth offence: permanent ban
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After each ban expires, the unbanner thread checks whether the IP re-offends. If it does, the next ban is longer. A Slack notification is sent on every transition so the operator always knows what is happening.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: Putting It All Together
&lt;/h2&gt;

&lt;p&gt;The daemon runs as &lt;strong&gt;four background threads&lt;/strong&gt; that all run simultaneously, forever:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thread 1: monitor   — tails the Nginx JSON log line by line (50ms poll)
Thread 2: baseline  — recalculates mean/stddev every 60 seconds
Thread 3: unbanner  — checks every 30 seconds if any bans have expired
Thread 4: dashboard — serves the live metrics webpage on port 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The monitor is the heart of the system. Here is the flow for every single log line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;New log line arrives from Nginx
          │
          ▼
Parse JSON → extract source_ip, timestamp, method, path, status, response_size
          │
          ▼
baseline.record_request(ip, ts, status)   ← always feeds the rolling window
          │
          ▼
Update sliding windows (per-IP deque and global deque)
Evict timestamps older than 60 seconds
Compute rate_per_s = len(deque) / 60
          │
          ▼
Is baseline mature? (sample_count &amp;gt;= 120)
    │
    ├── No  → skip detection, keep collecting data
    │
    └── Yes → run anomaly check
                │
                ├── Per-IP: zscore &amp;gt; 3.0 OR rate &amp;gt; 5x mean?
                │     Yes → iptables DROP + Slack ban alert (within 10 seconds)
                │
                └── Global: total rate anomalous?
                      Yes → Slack alert only (don't block everyone)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is event-driven — no polling, no sleeping between requests. The daemon reacts to each log line the moment Nginx writes it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 6: The Live Dashboard
&lt;/h2&gt;

&lt;p&gt;The dashboard is served by Python's built-in &lt;code&gt;http.server&lt;/code&gt; — no Flask, no FastAPI, no frameworks. Two routes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /&lt;/code&gt; — serves the HTML page&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/stats&lt;/code&gt; — returns a JSON snapshot of current state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The HTML page uses a 3-second JavaScript &lt;code&gt;setInterval&lt;/code&gt; to call &lt;code&gt;/api/stats&lt;/code&gt; and update the display without a page refresh:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/stats&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// effective_mean in req/s&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stddev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stddev&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// in req/s&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uptime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uptime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ... update banned IPs table, top 10 IPs, CPU/memory bars&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// every 3 seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dashboard shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Banned IPs&lt;/strong&gt; — with the condition that triggered the ban, rate, baseline mean at ban time, level, and time remaining&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global req/s&lt;/strong&gt; — current rate from the 60-second global window&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top 10 source IPs&lt;/strong&gt; — ranked by request count in the last 60 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU and memory usage&lt;/strong&gt; — from &lt;code&gt;psutil&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effective mean and stddev&lt;/strong&gt; — the real computed baseline values&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hourly slots&lt;/strong&gt; — the per-hour mean values, proving the baseline learns over time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uptime&lt;/strong&gt; — how long the daemon has been running&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with the data structure.&lt;/strong&gt; The choice of &lt;code&gt;deque&lt;/code&gt; over &lt;code&gt;list&lt;/code&gt; for the sliding window is what makes the system work under a real flood. O(1) vs O(n) on the eviction path is the difference between keeping up and falling behind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never hardcode effective_mean.&lt;/strong&gt; A fixed threshold makes assumptions about your traffic that will be wrong. A system that learns from its own data handles traffic spikes, quiet nights, and growth automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The proportional stddev floor is subtle but critical.&lt;/strong&gt; Without it, perfectly uniform traffic (like exactly 1 scanner per second) collapses stddev to zero and makes every fluctuation look like a 3-sigma event. The fix — &lt;code&gt;stddev &amp;gt;= mean * 0.3&lt;/code&gt; — scales with traffic and never substitutes a fake mean value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separation of concerns makes debugging much easier.&lt;/strong&gt; Because each module (monitor, baseline, detector, blocker, notifier) has one job, you can look at the audit log and immediately know which piece fired and why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The kernel is your friend.&lt;/strong&gt; iptables drops packets before your application software ever sees them. This is infinitely more efficient than rate-limiting inside Python — by the time Python code runs, the OS has already spent resources accepting the TCP connection.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Code
&lt;/h2&gt;

&lt;p&gt;The complete project is on GitHub: &lt;a href="https://github.com/lucadavid075/hng-anomaly-detector" rel="noopener noreferrer"&gt;github.com/lucadavid075/hng-anomaly-detector&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker Compose stack (Nginx + Nextcloud + Detector + Certbot)&lt;/li&gt;
&lt;li&gt;All Python source files, fully commented&lt;/li&gt;
&lt;li&gt;Slack-integrated alerting&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>devsecops</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
