<?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: zlynv</title>
    <description>The latest articles on DEV Community by zlynv (@zlynv).</description>
    <link>https://dev.to/zlynv</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4052266%2F7b17d90f-0f9b-4548-921b-22e79ea6739a.png</url>
      <title>DEV Community: zlynv</title>
      <link>https://dev.to/zlynv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zlynv"/>
    <language>en</language>
    <item>
      <title>Rate limiting algorithms explained: Token Bucket vs Sliding Window vs Fixed Window</title>
      <dc:creator>zlynv</dc:creator>
      <pubDate>Sun, 25 Oct 2026 04:18:58 +0000</pubDate>
      <link>https://dev.to/zlynv/rate-limiting-algorithms-explained-token-bucket-vs-sliding-window-vs-fixed-window-1630</link>
      <guid>https://dev.to/zlynv/rate-limiting-algorithms-explained-token-bucket-vs-sliding-window-vs-fixed-window-1630</guid>
      <description>&lt;p&gt;Rate limiting sounds simple: count requests, block when over the limit.&lt;br&gt;
But the algorithm you choose changes how your API behaves under load.&lt;br&gt;
Here's what's actually happening.&lt;/p&gt;
&lt;h2&gt;
  
  
  Fixed Window
&lt;/h2&gt;

&lt;p&gt;The simplest algorithm. Count requests in a fixed time window (e.g. 60&lt;br&gt;
seconds). When the counter hits the limit, block until the window resets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Window: [0s -------- 30s -------- 60s]
Requests:  1  2  3  4  5 ... 30 = DENY
                                        Window resets
Requests:  1  2  3  4  5 ... 30 = DENY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; boundary spikes. If the limit is 30/minute and a client sends&lt;br&gt;
30 requests at 59s and 30 at 61s, they got 60 requests in 2 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; login protection, payment endpoints, strict quotas.&lt;/p&gt;
&lt;h2&gt;
  
  
  Token Bucket
&lt;/h2&gt;

&lt;p&gt;Each client gets a bucket of tokens. Tokens refill at a constant rate.&lt;br&gt;
Each request costs one token. If the bucket is empty, the request is denied.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bucket: [========] 8/10 tokens
Request costs 1 token → [=======] 7/10
Refill 1 token/sec   → [========] 8/10 (after 1s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key property:&lt;/strong&gt; allows bursts. If you haven't made requests for a while,&lt;br&gt;
your bucket fills up and you can burst above the average rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; general API protection, where short bursts are acceptable.&lt;/p&gt;
&lt;h2&gt;
  
  
  Sliding Window
&lt;/h2&gt;

&lt;p&gt;Like Fixed Window, but the window slides continuously. Instead of counting&lt;br&gt;
in a fixed block, it counts requests in the last N seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Window:   [------======------]
           now-60s   now
Requests:  1  2  3  4  5  6 = 6 in last 60s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key property:&lt;/strong&gt; no boundary spikes. Smooth rate limiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; fair API usage, where you want consistent behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  GCRA (Generic Cell Rate Algorithm)
&lt;/h2&gt;

&lt;p&gt;Used in telecom networks. Instead of counting requests, it tracks the&lt;br&gt;
"theoretical arrival time" of the next allowed request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key property:&lt;/strong&gt; very smooth, memory-efficient, no bucket to overflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; high-throughput APIs, telecom-grade traffic shaping.&lt;/p&gt;
&lt;h2&gt;
  
  
  Leaky Bucket
&lt;/h2&gt;

&lt;p&gt;Requests enter a bucket that leaks at a constant rate. If the bucket&lt;br&gt;
overflows, requests are denied. Output is constant-rate regardless of&lt;br&gt;
input spikes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input:   [======]   (burst)
Bucket:  [====]     (buffered)
Output:  [=]        (constant rate)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; background jobs, queues, APIs that need constant processing rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Token Bucket&lt;/th&gt;
&lt;th&gt;Fixed Window&lt;/th&gt;
&lt;th&gt;Sliding Window&lt;/th&gt;
&lt;th&gt;GCRA&lt;/th&gt;
&lt;th&gt;Leaky Bucket&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bursts allowed&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boundary spikes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory per key&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smoothness&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Very high&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complexity&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The choice matters
&lt;/h2&gt;

&lt;p&gt;Most rate limiters default to Fixed Window because it's simple. But Fixed&lt;br&gt;
Window has the boundary spike problem: a clever client can get 2x the limit&lt;br&gt;
by timing requests around the window boundary.&lt;/p&gt;

&lt;p&gt;Token Bucket is usually the better default: it allows natural bursts while&lt;br&gt;
still enforcing the average rate. If you need strict quotas (no bursts at&lt;br&gt;
all), use Fixed Window explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  In Python
&lt;/h2&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;drogue.core.rules.rule&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AlgorithmType&lt;/span&gt;

&lt;span class="c1"&gt;# Burst-friendly (default)
&lt;/span&gt;&lt;span class="nd"&gt;@limiter.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Strict per-window quota
&lt;/span&gt;&lt;span class="nd"&gt;@limiter.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;algorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AlgorithmType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FIXED_WINDOW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Smooth, no boundary spikes
&lt;/span&gt;&lt;span class="nd"&gt;@limiter.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;algorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AlgorithmType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SLIDING_WINDOW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All five algorithms are available. Pick based on your traffic pattern.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://zlynv.github.io/drogue/concepts/algorithms/" rel="noopener noreferrer"&gt;Full algorithm docs&lt;/a&gt; |&lt;br&gt;
&lt;a href="https://github.com/zlynv/drogue" rel="noopener noreferrer"&gt;drogue on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>algorithms</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>How to rate limit FastAPI in 2026</title>
      <dc:creator>zlynv</dc:creator>
      <pubDate>Sun, 25 Oct 2026 04:13:07 +0000</pubDate>
      <link>https://dev.to/zlynv/how-to-rate-limit-fastapi-in-2026-1kbl</link>
      <guid>https://dev.to/zlynv/how-to-rate-limit-fastapi-in-2026-1kbl</guid>
      <description>&lt;p&gt;Rate limiting is the simplest defense against abuse. Here's how to add it&lt;br&gt;
to a FastAPI app in 2026, step by step.&lt;/p&gt;
&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;drogue[fastapi]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Basic rate limiting
&lt;/h2&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;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;drogue.adapters.fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DrogueLimiter&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DrogueLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_limits&lt;/span&gt;&lt;span class="o"&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;100/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@limiter.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&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;value&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;p&gt;That's it. Every request gets a limit. Exceed it and you get 429 with&lt;br&gt;
standard headers: &lt;code&gt;X-RateLimit-Limit&lt;/code&gt;, &lt;code&gt;X-RateLimit-Remaining&lt;/code&gt;, &lt;code&gt;Retry-After&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Per-route limits
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/public&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@limiter.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;100/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;public&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@limiter.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# strict: brute force protection
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&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;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/upload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@limiter.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UploadFile&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filename&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No &lt;code&gt;request: Request&lt;/code&gt; needed. drogue extracts identity from the ASGI&lt;br&gt;
context automatically.&lt;/p&gt;
&lt;h2&gt;
  
  
  Choosing an algorithm
&lt;/h2&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;drogue.core.rules.rule&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AlgorithmType&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@limiter.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;algorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AlgorithmType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FIXED_WINDOW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&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;value&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;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Algorithm&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;th&gt;Use when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;TOKEN_BUCKET&lt;/code&gt; (default)&lt;/td&gt;
&lt;td&gt;Allows short bursts above average rate&lt;/td&gt;
&lt;td&gt;General API protection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FIXED_WINDOW&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hard cap per window, no bursts&lt;/td&gt;
&lt;td&gt;Login, payment, strict quotas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SLIDING_WINDOW&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Smooth rate limiting, no boundary spikes&lt;/td&gt;
&lt;td&gt;Fair API usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GCRA&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Telecom-grade smooth traffic&lt;/td&gt;
&lt;td&gt;High-throughput APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LEAKY_BUCKET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Constant-rate processing&lt;/td&gt;
&lt;td&gt;Background jobs, queues&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Token Bucket is the default because it's burst-friendly. For brute force&lt;br&gt;
protection on login endpoints, use &lt;code&gt;FIXED_WINDOW&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Custom identity (not just IP)
&lt;/h2&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;drogue.core.identity&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HeaderExtractor&lt;/span&gt;

&lt;span class="n"&gt;api_key_extractor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HeaderExtractor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@limiter.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;100/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;api_key_extractor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&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;value&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;p&gt;Available extractors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RemoteAddressExtractor&lt;/code&gt; (default) - client IP with X-Forwarded-For anti-spoof&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UserExtractor&lt;/code&gt; - user ID from request context&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HeaderExtractor&lt;/code&gt; - custom header value&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CompositeExtractor&lt;/code&gt; - combine multiple&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Redis for multi-process
&lt;/h2&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;drogue.core.storage.redis&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RedisStorage&lt;/span&gt;

&lt;span class="n"&gt;storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RedisStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis://localhost:6379&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DrogueLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_limits&lt;/span&gt;&lt;span class="o"&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;100/minute&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;p&gt;With Redis, rate limits are shared across all workers and processes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Custom 429 response
&lt;/h2&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;starlette.responses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;JSONResponse&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;my_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&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;error&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;slow_down&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;retry_after&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DrogueLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_renderer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;my_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Rate limit headers are injected automatically.&lt;/p&gt;
&lt;h2&gt;
  
  
  WebSocket rate limiting
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.websocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/ws/chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@limiter.limit_ws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DDoS detection&lt;/strong&gt; - Z-score anomaly detection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-ban&lt;/strong&gt; - progressive bans for repeat offenders&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust scoring&lt;/strong&gt; - 9x throughput for verified users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CAPTCHA challenges&lt;/strong&gt; - Turnstile integration for page traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All off by default. Enable what you need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;drogue[fastapi]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://zlynv.github.io/drogue/" rel="noopener noreferrer"&gt;Full docs&lt;/a&gt; |&lt;br&gt;
&lt;a href="https://github.com/zlynv/drogue" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Python rate limiting in 2026: drogue vs slowapi vs limits vs flask-limiter</title>
      <dc:creator>zlynv</dc:creator>
      <pubDate>Sun, 25 Oct 2026 03:54:31 +0000</pubDate>
      <link>https://dev.to/zlynv/python-rate-limiting-in-2026-drogue-vs-slowapi-vs-limits-vs-flask-limiter-3fe7</link>
      <guid>https://dev.to/zlynv/python-rate-limiting-in-2026-drogue-vs-slowapi-vs-limits-vs-flask-limiter-3fe7</guid>
      <description>&lt;p&gt;You're adding rate limiting to a Python web app. The usual suspects are slowapi, flask-limiter, django-ratelimit, and the &lt;code&gt;limits&lt;/code&gt; library. There's also drogue. Here's how they compare.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;drogue&lt;/th&gt;
&lt;th&gt;slowapi&lt;/th&gt;
&lt;th&gt;flask-limiter&lt;/th&gt;
&lt;th&gt;limits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;request: Request&lt;/code&gt; in signatures&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;N/A (standalone)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FastAPI&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Via adapter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Django&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Via adapter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flask&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Via adapter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Algorithms&lt;/td&gt;
&lt;td&gt;5 (Token Bucket, Sliding, Fixed, GCRA, Leaky)&lt;/td&gt;
&lt;td&gt;1 (Fixed Window)&lt;/td&gt;
&lt;td&gt;1 (Fixed Window)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DDoS detection&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-ban&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CAPTCHA challenges&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shadow mode&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom 429 body&lt;/td&gt;
&lt;td&gt;&lt;code&gt;error_renderer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Custom handler&lt;/td&gt;
&lt;td&gt;Custom handler&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero runtime deps&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (starlette)&lt;/td&gt;
&lt;td&gt;No (flask)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;request: Request&lt;/code&gt; problem
&lt;/h2&gt;

&lt;p&gt;This is the biggest difference and the reason drogue exists.&lt;/p&gt;

&lt;p&gt;slowapi requires you to add &lt;code&gt;request: Request&lt;/code&gt; to every decorated endpoint:&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;# slowapi
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;slowapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Limiter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;slowapi.util&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_remote_address&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.requests&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;

&lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Limiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;get_remote_address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@limiter.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;   &lt;span class="c1"&gt;# rate limiter leaked into your signature
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&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;value&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;p&gt;Now your business logic is coupled to the rate limiter. Change the limiter,&lt;br&gt;
rewrite every endpoint. Test the endpoint without HTTP, mock the request.&lt;/p&gt;

&lt;p&gt;drogue extracts identity from the ASGI context without touching your signature:&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;# drogue
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;drogue.adapters.fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DrogueLimiter&lt;/span&gt;

&lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DrogueLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@limiter.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;                   &lt;span class="c1"&gt;# clean
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&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;value&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;h2&gt;
  
  
  Algorithm choice matters
&lt;/h2&gt;

&lt;p&gt;slowapi uses Fixed Window only. &lt;code&gt;"30/minute"&lt;/code&gt; means exactly 30 requests,&lt;br&gt;
then hard stop. That's fine for strict quotas but harsh on bursty traffic.&lt;/p&gt;

&lt;p&gt;drogue defaults to Token Bucket, which allows short bursts above the average&lt;br&gt;
rate. A &lt;code&gt;"30/minute"&lt;/code&gt; limit with Token Bucket might briefly allow 35 requests,&lt;br&gt;
then smooth out. For strict quotas, use &lt;code&gt;algorithm=AlgorithmType.FIXED_WINDOW&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The choice between burst-friendly and strict-per-window is a design decision,&lt;br&gt;
not a bug. slowapi just doesn't offer the option.&lt;/p&gt;
&lt;h2&gt;
  
  
  WebSocket rate limiting
&lt;/h2&gt;

&lt;p&gt;Neither slowapi nor flask-limiter supports WebSocket rate limiting. If you&lt;br&gt;
have real-time features (chat, notifications, games), you need drogue or&lt;br&gt;
roll your own.&lt;/p&gt;
&lt;h2&gt;
  
  
  Built-in defense layers
&lt;/h2&gt;

&lt;p&gt;drogue goes beyond rate limiting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DDoS detection&lt;/strong&gt; - Z-score anomaly detection across client rate distributions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-ban&lt;/strong&gt; - progressive ban with doubling duration (5m to 160m)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust scoring&lt;/strong&gt; - 9x throughput for verified users (state machine: Unknown/Normal/Trusted/Distrusted/Banned)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CAPTCHA challenges&lt;/strong&gt; - Turnstile integration with signed pass cookies and replay-proof tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Circuit breaker&lt;/strong&gt; - Closed/Open/HalfOpen states for failing backends&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow mode&lt;/strong&gt; - test rules without enforcing, collect metrics first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All off by default. Enable what you need.&lt;/p&gt;
&lt;h2&gt;
  
  
  Custom 429 responses
&lt;/h2&gt;

&lt;p&gt;slowapi makes you replace the exception handler entirely. drogue gives you&lt;br&gt;
a callback:&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;starlette.responses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;JSONResponse&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;my_renderer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&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;error&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;rate_limited&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;retry_after&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DrogueLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_renderer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;my_renderer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rate limit headers (&lt;code&gt;X-RateLimit-Limit&lt;/code&gt;, &lt;code&gt;X-RateLimit-Remaining&lt;/code&gt;, &lt;code&gt;Retry-After&lt;/code&gt;)&lt;br&gt;
are injected automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use which
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use drogue if&lt;/strong&gt; you want clean signatures, multiple algorithms, WebSocket&lt;br&gt;
support, defense layers, or you're migrating from slowapi and want less code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use slowapi if&lt;/strong&gt; you're on FastAPI only, already have it integrated, and&lt;br&gt;
don't mind &lt;code&gt;request: Request&lt;/code&gt; in your signatures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use flask-limiter if&lt;/strong&gt; you're on Flask and want something battle-tested.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use limits if&lt;/strong&gt; you need a standalone rate limiter (CLI tools, scripts, etc.)&lt;br&gt;
without a web framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;drogue[fastapi]   &lt;span class="c"&gt;# or [django], [flask], [drf]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/zlynv/drogue" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; |&lt;br&gt;
&lt;a href="https://zlynv.github.io/drogue/" rel="noopener noreferrer"&gt;Full docs&lt;/a&gt; |&lt;br&gt;
&lt;a href="https://zlynv.github.io/drogue/migration/slowapi/" rel="noopener noreferrer"&gt;Migration guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>django</category>
      <category>flask</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Every Python rate limiter has this DDoS blind spot — I built one that doesn't</title>
      <dc:creator>zlynv</dc:creator>
      <pubDate>Sun, 02 Aug 2026 11:45:25 +0000</pubDate>
      <link>https://dev.to/zlynv/every-python-rate-limiter-has-this-ddos-blind-spot-i-built-one-that-doesnt-1260</link>
      <guid>https://dev.to/zlynv/every-python-rate-limiter-has-this-ddos-blind-spot-i-built-one-that-doesnt-1260</guid>
      <description>&lt;h1&gt;
  
  
  Every Python rate limiter has this DDoS blind spot — I built one that doesn't
&lt;/h1&gt;

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

&lt;p&gt;But your server is drowning.&lt;/p&gt;

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

&lt;p&gt;So I built &lt;a href="https://github.com/zlynv/drogue" rel="noopener noreferrer"&gt;drogue&lt;/a&gt; — and tested it under real DDoS traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The blind spot
&lt;/h2&gt;

&lt;p&gt;Every rate limiter I tested works the same way:&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;# This catches overuse. It doesn't catch distributed attacks.
&lt;/span&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@limiter.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;100/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&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;value&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;p&gt;The problem: &lt;strong&gt;rate limiting and DDoS detection are different problems.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; catches single clients exceeding limits. It misses distributed attacks where each client stays under the limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DDoS detection&lt;/strong&gt; catches anomalous traffic patterns. It's a different layer entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most Python libraries only solve the first one. I needed both.&lt;/p&gt;




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

&lt;p&gt;I benchmarked 5 rate limiting approaches under simulated DDoS traffic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;slowapi&lt;/strong&gt;: Rate limiting only. No DDoS detection. No WebSocket support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;flask-limiter&lt;/strong&gt;: Rate limiting only. Flask-specific. No DDoS detection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pyrate-limiter&lt;/strong&gt;: Rate limiting only. Algorithm-focused. No framework adapters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ratethrottle&lt;/strong&gt;: Rate limiting + DDoS detection + WebSocket support. No trust system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;drogue&lt;/strong&gt;: Rate limiting + DDoS detection + WebSocket support + trust state machine + circuit breaker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key difference: drogue has a &lt;strong&gt;Z-score anomaly detector&lt;/strong&gt; that learns what "normal" traffic looks like and flags statistical outliers.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;drogue adds two layers that rate limiters miss:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Z-score anomaly detection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Progressive bans&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After 5 violations, bans escalate automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Level 1: 1 minute&lt;/li&gt;
&lt;li&gt;Level 2: 10 minutes&lt;/li&gt;
&lt;li&gt;Level 3: 1 hour&lt;/li&gt;
&lt;li&gt;Level 4: 24 hours&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Most libraries only count requests. drogue detects attacks.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;p&gt;I ran a DDoS simulation with 50 concurrent users (mix of normal users and attackers):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total requests&lt;/td&gt;
&lt;td&gt;42,815&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requests/sec&lt;/td&gt;
&lt;td&gt;2,173.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attackers banned (403)&lt;/td&gt;
&lt;td&gt;97.8% of attacker traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limited (429)&lt;/td&gt;
&lt;td&gt;0.02%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Successful attacks&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;97.8% of requests from attackers were banned.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;Response time overhead: p50=5ms, p95=7ms, p99=10ms. Less than 10ms for most applications.&lt;/p&gt;




&lt;h2&gt;
  
  
  The feature comparison
&lt;/h2&gt;

&lt;p&gt;Here's what drogue includes that others don't:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;drogue&lt;/th&gt;
&lt;th&gt;slowapi&lt;/th&gt;
&lt;th&gt;flask-limiter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DDoS detection&lt;/td&gt;
&lt;td&gt;Z-score&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Progressive bans&lt;/td&gt;
&lt;td&gt;Auto-escalating&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust state machine&lt;/td&gt;
&lt;td&gt;7 states&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket protection&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Circuit breaker&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 algorithms&lt;/td&gt;
&lt;td&gt;TB/SW/FW/GCRA/LB&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framework support&lt;/td&gt;
&lt;td&gt;FastAPI/Django/Flask/DRF&lt;/td&gt;
&lt;td&gt;FastAPI&lt;/td&gt;
&lt;td&gt;Flask&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What I got wrong
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Where drogue is today&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  The counterintuitive truth
&lt;/h2&gt;

&lt;p&gt;The fastest rate limiter isn't the best one.&lt;/p&gt;

&lt;p&gt;drogue is slower than slowapi. 5ms overhead vs 2ms. That's a 2.5x difference. In benchmarks, slowapi wins.&lt;/p&gt;

&lt;p&gt;But slowapi doesn't detect DDoS attacks. It doesn't ban attackers. It doesn't learn what "normal" traffic looks like.&lt;/p&gt;

&lt;p&gt;Protection isn't about speed. It's about coverage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;drogue[fastapi]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;drogue.adapters.fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DrogueLimiter&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DrogueLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_limits&lt;/span&gt;&lt;span class="o"&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;100/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@limiter.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&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;value&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;p&gt;That's it. No &lt;code&gt;request: Request&lt;/code&gt;. No configuration files. DDoS protection built in.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;drogue v0.2.0 is out now with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GCRA + Leaky Bucket algorithms&lt;/li&gt;
&lt;li&gt;MongoDB storage backend&lt;/li&gt;
&lt;li&gt;Thread safety guarantees&lt;/li&gt;
&lt;li&gt;Full benchmark suite&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;v0.3&lt;/strong&gt; will add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redis-backed ban persistence&lt;/li&gt;
&lt;li&gt;Trust cache sync across workers&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it out
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/zlynv/drogue" rel="noopener noreferrer"&gt;github.com/zlynv/drogue&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyPI&lt;/strong&gt;: &lt;a href="https://pypi.org/project/drogue/" rel="noopener noreferrer"&gt;pypi.org/project/drogue&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs&lt;/strong&gt;: &lt;a href="https://zlynv.github.io/drogue/" rel="noopener noreferrer"&gt;zlynv.github.io/drogue&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Have you been hit by a distributed attack that your rate limiter missed? I'd love to hear about it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>Building a Modern Rate Limiter and DDoS Protection Library for Python</title>
      <dc:creator>zlynv</dc:creator>
      <pubDate>Wed, 29 Jul 2026 03:31:10 +0000</pubDate>
      <link>https://dev.to/zlynv/building-a-modern-rate-limiter-and-ddos-protection-library-for-python-49fe</link>
      <guid>https://dev.to/zlynv/building-a-modern-rate-limiter-and-ddos-protection-library-for-python-49fe</guid>
      <description>&lt;p&gt;Rate limiting is one of those features every production API eventually needs.&lt;/p&gt;

&lt;p&gt;Whether you're building a public REST API, a WebSocket service, or an authentication endpoint, you'll eventually face problems like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Credential stuffing&lt;/li&gt;
&lt;li&gt;Brute-force attacks&lt;/li&gt;
&lt;li&gt;API abuse&lt;/li&gt;
&lt;li&gt;Bots scraping your endpoints&lt;/li&gt;
&lt;li&gt;Unexpected traffic spikes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most applications solve this with a simple request counter.&lt;/p&gt;

&lt;p&gt;But after building several APIs with Django, FastAPI, and Flask, I realized that production traffic requires much more than "X requests per minute."&lt;/p&gt;

&lt;p&gt;That observation led me to build &lt;strong&gt;drogue&lt;/strong&gt;, an open-source Python library for rate limiting and traffic protection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Traditional rate limiting is straightforward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Allow 100 requests per minute.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This works well for many cases, but real-world applications quickly expose its limitations.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A distributed attack can remain below the per-IP limit.&lt;/li&gt;
&lt;li&gt;A bot can rotate through proxies.&lt;/li&gt;
&lt;li&gt;WebSocket connections often require different handling than HTTP requests.&lt;/li&gt;
&lt;li&gt;Different endpoints need different protection strategies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted a system that could go beyond simple request counting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Goals
&lt;/h2&gt;

&lt;p&gt;From the beginning, I focused on a few principles.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Clean framework integration
&lt;/h3&gt;

&lt;p&gt;I didn't want endpoint functions filled with framework-specific plumbing.&lt;/p&gt;

&lt;p&gt;Instead, the library should feel like a natural extension of the framework.&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;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;drogue.adapters.fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DrogueLimiter&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DrogueLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_limits&lt;/span&gt;&lt;span class="o"&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;100/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@limiter.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;users&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;ok&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;p&gt;No additional request objects.&lt;br&gt;
No complicated middleware configuration.&lt;br&gt;
Minimal boilerplate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multiple Rate Limiting Algorithms
&lt;/h2&gt;

&lt;p&gt;Different applications require different algorithms.&lt;/p&gt;

&lt;p&gt;Instead of supporting only one approach, drogue includes multiple options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Token Bucket&lt;/li&gt;
&lt;li&gt;Sliding Window&lt;/li&gt;
&lt;li&gt;Fixed Window&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each has different trade-offs between accuracy, burst handling, and memory usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond Rate Limiting
&lt;/h2&gt;

&lt;p&gt;One thing I kept noticing was that abusive traffic often doesn't violate the configured limits.&lt;/p&gt;

&lt;p&gt;An attacker may intentionally stay below the threshold.&lt;/p&gt;

&lt;p&gt;That's why I started experimenting with additional protection mechanisms such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Traffic anomaly detection&lt;/li&gt;
&lt;li&gt;Progressive temporary bans&lt;/li&gt;
&lt;li&gt;Probe detection&lt;/li&gt;
&lt;li&gt;Circuit breakers&lt;/li&gt;
&lt;li&gt;CIDR filtering&lt;/li&gt;
&lt;li&gt;Shadow mode for safely testing rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rather than replacing traditional rate limiting, these features complement it.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebSocket Support
&lt;/h2&gt;

&lt;p&gt;Many existing rate limiting solutions primarily focus on HTTP.&lt;/p&gt;

&lt;p&gt;Modern applications increasingly rely on WebSockets for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chat applications&lt;/li&gt;
&lt;li&gt;Live dashboards&lt;/li&gt;
&lt;li&gt;Multiplayer games&lt;/li&gt;
&lt;li&gt;Real-time notifications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Supporting both HTTP and WebSocket traffic from the same library became an important design goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Framework Support
&lt;/h2&gt;

&lt;p&gt;Currently, drogue provides adapters for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Django&lt;/li&gt;
&lt;li&gt;Django REST Framework&lt;/li&gt;
&lt;li&gt;FastAPI&lt;/li&gt;
&lt;li&gt;Flask&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to keep the developer experience as consistent as possible across frameworks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;Performance matters because rate limiting runs on every request.&lt;/p&gt;

&lt;p&gt;The core package is designed with minimal overhead while keeping the implementation extensible enough for different storage backends and deployment scenarios.&lt;/p&gt;

&lt;p&gt;Performance testing and optimization remain an ongoing part of development, and I plan to continue publishing benchmark improvements as the project evolves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Building this project taught me several things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Good APIs are often harder to design than efficient algorithms.&lt;/li&gt;
&lt;li&gt;Framework integration significantly affects developer experience.&lt;/li&gt;
&lt;li&gt;Rate limiting alone isn't enough for many production environments.&lt;/li&gt;
&lt;li&gt;Clear documentation is just as important as clean code.&lt;/li&gt;
&lt;li&gt;Simplicity usually wins over feature overload.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;There are still many ideas I'd like to explore, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Additional storage backends&lt;/li&gt;
&lt;li&gt;More adaptive protection strategies&lt;/li&gt;
&lt;li&gt;Better observability&lt;/li&gt;
&lt;li&gt;Additional framework integrations&lt;/li&gt;
&lt;li&gt;More comprehensive benchmarking&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  I'd Love Your Feedback
&lt;/h2&gt;

&lt;p&gt;This project is still evolving, and feedback from the Python community is incredibly valuable.&lt;/p&gt;

&lt;p&gt;If you build production APIs with Django, FastAPI, Flask, or another Python framework, I'd love to hear your thoughts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What features do you look for in a rate limiting library?&lt;/li&gt;
&lt;li&gt;What challenges have you encountered protecting public APIs?&lt;/li&gt;
&lt;li&gt;Is there something missing that would make a library like this more useful?&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The project is open source, and contributions, suggestions, and constructive criticism are always welcome.&lt;/p&gt;

&lt;p&gt;Building infrastructure libraries is always a balancing act between performance, flexibility, and developer experience. Drogue is my attempt to make API protection easier without sacrificing clean application code.&lt;/p&gt;

&lt;p&gt;If you're building production APIs with Django, FastAPI, Flask, or another Python framework, I'd love to hear your thoughts. Whether it's an issue, feature request, benchmark, or pull request, your feedback will help shape the project.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/zlynv/drogue" rel="noopener noreferrer"&gt;https://github.com/zlynv/drogue&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://zlynv.github.io/drogue/" rel="noopener noreferrer"&gt;https://zlynv.github.io/drogue/&lt;/a&gt;&lt;br&gt;
PyPI: &lt;code&gt;pip install drogue&lt;/code&gt;&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>django</category>
      <category>flask</category>
      <category>python</category>
    </item>
  </channel>
</rss>
