<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://bnacar.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://bnacar.dev/" rel="alternate" type="text/html" /><updated>2026-08-09T22:23:02+00:00</updated><id>https://bnacar.dev/feed.xml</id><title type="html">Burhanettin Nacar</title><subtitle>Senior backend engineer writing about distributed systems, Java, applied AI, and production software engineering.</subtitle><author><name>Burhanettin Nacar</name></author><entry><title type="html">How to Extract Tables from PDFs Using Python (Without Losing Your Mind)</title><link href="https://bnacar.dev/2026/01/13/how-to-extract-tables-from-pdfs-using-python.html" rel="alternate" type="text/html" title="How to Extract Tables from PDFs Using Python (Without Losing Your Mind)" /><published>2026-01-13T00:00:00+00:00</published><updated>2026-01-13T00:00:00+00:00</updated><id>https://bnacar.dev/2026/01/13/how-to-extract-tables-from-pdfs-using-python</id><content type="html" xml:base="https://bnacar.dev/2026/01/13/how-to-extract-tables-from-pdfs-using-python.html"><![CDATA[<p>If you’ve ever tried to extract data from a PDF, you know the pain. What looks like a simple table on screen is actually a chaotic mess of positioned text elements in the file.</p>

<p>I built a PDF extraction API for a real project and ended up learning more about PDF internals than I expected. Here’s a practical breakdown.</p>

<h2 id="the-problem-pdfs-dont-have-tables">The Problem: PDFs Don’t Have “Tables”</h2>

<p>Open any PDF with tabular data. It looks organized, right? Rows, columns, headers.</p>

<p>Now look at what’s actually in the file:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>draw "Product" at position (50, 100)
draw "Price" at position (200, 100)
draw "Widget" at position (50, 120)
draw "$99" at position (200, 120)
</code></pre></div></div>

<p>There’s no table structure. No rows. No columns. Just text floating at coordinates.</p>

<p>Your job is to reconstruct the logical structure from spatial positions.</p>

<h2 id="approach-1-pymupdf-basic-text-extraction">Approach 1: PyMuPDF (Basic Text Extraction)</h2>

<p>For simple text extraction, PyMuPDF (also called <code class="language-plaintext highlighter-rouge">fitz</code>) is fast and reliable:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">fitz</span>

<span class="k">def</span> <span class="nf">extract_text</span><span class="p">(</span><span class="n">pdf_path</span><span class="p">):</span>
    <span class="n">doc</span> <span class="o">=</span> <span class="n">fitz</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="n">pdf_path</span><span class="p">)</span>
    <span class="n">text</span> <span class="o">=</span> <span class="s">""</span>
    <span class="k">for</span> <span class="n">page</span> <span class="ow">in</span> <span class="n">doc</span><span class="p">:</span>
        <span class="n">text</span> <span class="o">+=</span> <span class="n">page</span><span class="p">.</span><span class="n">get_text</span><span class="p">()</span>
    <span class="k">return</span> <span class="n">text</span>
</code></pre></div></div>

<p><strong>Pros:</strong> Fast, handles most PDFs<br />
<strong>Cons:</strong> Tables come out as jumbled text</p>

<p>Output from a table:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Product Price Quantity
Widget $99 10
Gadget $149 5
</code></pre></div></div>

<p>Not useful if you need structured data.</p>

<h2 id="approach-2-pdfplumber-table-detection">Approach 2: pdfplumber (Table Detection)</h2>

<p>pdfplumber is specifically designed for table extraction:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pdfplumber</span>

<span class="k">def</span> <span class="nf">extract_tables</span><span class="p">(</span><span class="n">pdf_path</span><span class="p">):</span>
    <span class="n">tables</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">with</span> <span class="n">pdfplumber</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="n">pdf_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">pdf</span><span class="p">:</span>
        <span class="k">for</span> <span class="n">page</span> <span class="ow">in</span> <span class="n">pdf</span><span class="p">.</span><span class="n">pages</span><span class="p">:</span>
            <span class="n">page_tables</span> <span class="o">=</span> <span class="n">page</span><span class="p">.</span><span class="n">extract_tables</span><span class="p">()</span>
            <span class="n">tables</span><span class="p">.</span><span class="n">extend</span><span class="p">(</span><span class="n">page_tables</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">tables</span>
</code></pre></div></div>

<p><strong>Pros:</strong> Detects table boundaries automatically<br />
<strong>Cons:</strong> Struggles with complex layouts, merged cells</p>

<p>Output:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span>
    <span class="p">[[</span><span class="s">'Product'</span><span class="p">,</span> <span class="s">'Price'</span><span class="p">,</span> <span class="s">'Quantity'</span><span class="p">],</span>
     <span class="p">[</span><span class="s">'Widget'</span><span class="p">,</span> <span class="s">'$99'</span><span class="p">,</span> <span class="s">'10'</span><span class="p">],</span>
     <span class="p">[</span><span class="s">'Gadget'</span><span class="p">,</span> <span class="s">'$149'</span><span class="p">,</span> <span class="s">'5'</span><span class="p">]]</span>
<span class="p">]</span>
</code></pre></div></div>

<p>Much better! But still needs post-processing.</p>

<h2 id="approach-3-combining-both">Approach 3: Combining Both</h2>

<p>The best results come from combining approaches:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">fitz</span>
<span class="kn">import</span> <span class="nn">pdfplumber</span>

<span class="k">def</span> <span class="nf">smart_extract</span><span class="p">(</span><span class="n">pdf_path</span><span class="p">):</span>
    <span class="c1"># First, check if PDF has selectable text
</span>    <span class="n">doc</span> <span class="o">=</span> <span class="n">fitz</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="n">pdf_path</span><span class="p">)</span>
    <span class="n">first_page_text</span> <span class="o">=</span> <span class="n">doc</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">get_text</span><span class="p">().</span><span class="n">strip</span><span class="p">()</span>
    
    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">first_page_text</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">50</span><span class="p">:</span>
        <span class="c1"># Likely a scanned PDF - needs OCR
</span>        <span class="k">return</span> <span class="p">{</span><span class="s">"error"</span><span class="p">:</span> <span class="s">"Scanned PDF detected, OCR required"</span><span class="p">}</span>
    
    <span class="c1"># Extract tables with pdfplumber
</span>    <span class="n">tables</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">with</span> <span class="n">pdfplumber</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="n">pdf_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">pdf</span><span class="p">:</span>
        <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">page</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">pdf</span><span class="p">.</span><span class="n">pages</span><span class="p">):</span>
            <span class="k">for</span> <span class="n">table</span> <span class="ow">in</span> <span class="n">page</span><span class="p">.</span><span class="n">extract_tables</span><span class="p">():</span>
                <span class="k">if</span> <span class="n">table</span> <span class="ow">and</span> <span class="nb">len</span><span class="p">(</span><span class="n">table</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">:</span>
                    <span class="n">headers</span> <span class="o">=</span> <span class="n">table</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
                    <span class="n">rows</span> <span class="o">=</span> <span class="n">table</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>
                    <span class="n">tables</span><span class="p">.</span><span class="n">append</span><span class="p">({</span>
                        <span class="s">"page"</span><span class="p">:</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span>
                        <span class="s">"headers"</span><span class="p">:</span> <span class="n">headers</span><span class="p">,</span>
                        <span class="s">"rows"</span><span class="p">:</span> <span class="n">rows</span>
                    <span class="p">})</span>
    
    <span class="c1"># Extract remaining text with PyMuPDF
</span>    <span class="n">full_text</span> <span class="o">=</span> <span class="s">""</span>
    <span class="k">for</span> <span class="n">page</span> <span class="ow">in</span> <span class="n">doc</span><span class="p">:</span>
        <span class="n">full_text</span> <span class="o">+=</span> <span class="n">page</span><span class="p">.</span><span class="n">get_text</span><span class="p">()</span>
    
    <span class="k">return</span> <span class="p">{</span>
        <span class="s">"tables"</span><span class="p">:</span> <span class="n">tables</span><span class="p">,</span>
        <span class="s">"text"</span><span class="p">:</span> <span class="n">full_text</span><span class="p">,</span>
        <span class="s">"page_count"</span><span class="p">:</span> <span class="nb">len</span><span class="p">(</span><span class="n">doc</span><span class="p">)</span>
    <span class="p">}</span>
</code></pre></div></div>

<h2 id="the-hard-parts-nobody-tells-you-about">The Hard Parts Nobody Tells You About</h2>

<h3 id="1-table-boundaries-are-ambiguous">1. Table boundaries are ambiguous</h3>

<p>Is this one table or two?</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Name     | Email
---------|------------------
John     | john@example.com

Department | Budget
-----------|--------
Sales      | $50,000
</code></pre></div></div>

<p>Humans see two tables. Algorithms often merge them.</p>

<h3 id="2-headers-arent-always-on-top">2. Headers aren’t always on top</h3>

<p>Some invoices put totals at the bottom. Some have headers on the left side. Some have no headers at all.</p>

<h3 id="3-multi-page-tables">3. Multi-page tables</h3>

<p>When a table spans pages, you need to:</p>
<ul>
  <li>Detect it’s a continuation (no headers on page 2)</li>
  <li>Merge rows correctly</li>
  <li>Handle page breaks mid-row</li>
</ul>

<h3 id="4-currency-and-number-parsing">4. Currency and number parsing</h3>

<p>“$1,234.56” vs “1.234,56 EUR” vs “JPY 1234”</p>

<p>Different locales, different formats. Don’t assume.</p>

<h2 id="a-better-way-use-an-api">A Better Way: Use an API</h2>

<p>After building all this myself, I packaged it into an API so others don’t have to:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST <span class="s2">"https://pdfpull-895295000838.europe-west1.run.app/api/v1/extract/tables"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"X-API-Key: sk_demo_123456789"</span> <span class="se">\</span>
  <span class="nt">-F</span> <span class="s2">"file=@invoice.pdf"</span>
</code></pre></div></div>

<p>Response:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"tables"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"page_number"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="nl">"headers"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"Product"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Price"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Qty"</span><span class="p">],</span><span class="w">
      </span><span class="nl">"rows"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">[</span><span class="s2">"Widget"</span><span class="p">,</span><span class="w"> </span><span class="s2">"$99"</span><span class="p">,</span><span class="w"> </span><span class="s2">"10"</span><span class="p">],</span><span class="w">
        </span><span class="p">[</span><span class="s2">"Gadget"</span><span class="p">,</span><span class="w"> </span><span class="s2">"$149"</span><span class="p">,</span><span class="w"> </span><span class="s2">"5"</span><span class="p">]</span><span class="w">
      </span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"table_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>It also has smart parsers for invoices and resumes that extract specific fields:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST <span class="s2">"https://pdfpull-895295000838.europe-west1.run.app/api/v1/parse/invoice"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"X-API-Key: sk_demo_123456789"</span> <span class="se">\</span>
  <span class="nt">-F</span> <span class="s2">"file=@invoice.pdf"</span>
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"vendor_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ACME Corporation"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"invoice_number"</span><span class="p">:</span><span class="w"> </span><span class="s2">"INV-2024-0042"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"invoice_date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"January 15, 2024"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"total_amount"</span><span class="p">:</span><span class="w"> </span><span class="mf">1250.00</span><span class="p">,</span><span class="w">
  </span><span class="nl">"currency"</span><span class="p">:</span><span class="w"> </span><span class="s2">"USD"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"line_items"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Widget"</span><span class="p">,</span><span class="w"> </span><span class="nl">"quantity"</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="nl">"amount"</span><span class="p">:</span><span class="w"> </span><span class="mf">990.00</span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Gadget"</span><span class="p">,</span><span class="w"> </span><span class="nl">"quantity"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w"> </span><span class="nl">"amount"</span><span class="p">:</span><span class="w"> </span><span class="mf">260.00</span><span class="p">}</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.91</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>PDF extraction is harder than it looks. If you’re building something that only occasionally needs PDF parsing, use a library. If you’re doing it at scale, consider an API that handles the edge cases for you.</p>

<p>If you need consistent results across lots of PDFs, an API can save you time. For one-off jobs, a library is usually enough.</p>

<hr />

<p>Building this in public. Follow along on Twitter: <a href="https://twitter.com/uppnrise">@uppnrise</a></p>]]></content><author><name>Burhanettin Nacar</name></author><category term="Python" /><category term="API" /><category term="PDF" /><category term="Tutorial" /><summary type="html"><![CDATA[A practical guide to extracting tables from PDFs with PyMuPDF and pdfplumber, plus pitfalls and an API option for scale.]]></summary></entry><entry><title type="html">Machine Learning Fundamentals: What I Wish Someone Had Told Me Earlier</title><link href="https://bnacar.dev/2025/11/25/machine-learning-fundamentals-explained-simply.html" rel="alternate" type="text/html" title="Machine Learning Fundamentals: What I Wish Someone Had Told Me Earlier" /><published>2025-11-25T00:00:00+00:00</published><updated>2025-11-25T00:00:00+00:00</updated><id>https://bnacar.dev/2025/11/25/machine-learning-fundamentals-explained-simply</id><content type="html" xml:base="https://bnacar.dev/2025/11/25/machine-learning-fundamentals-explained-simply.html"><![CDATA[<p>A few months ago, I was having coffee with a friend who’s a product manager. She asked me what “gradient descent” means, and I completely blanked. Not because I don’t know it—I use it every day—but because I realized I’d never actually had to explain it to someone outside the ML bubble.</p>

<p>That conversation stuck with me. So here’s my attempt to explain the core ML concepts the way I wish someone had explained them to me when I started.</p>

<h2 id="supervised-vs-unsupervised-learning-the-study-group-analogy">Supervised vs. Unsupervised Learning: The Study Group Analogy</h2>

<p>Imagine you’re back in school, studying for an exam.</p>

<p><strong>Supervised learning</strong> is like studying with a really good answer key. You look at a practice problem, try to solve it, then check the answer. Got it wrong? You adjust your thinking. Over time, you start recognizing patterns—”Oh, whenever I see X, the answer is usually Y.”</p>

<p>That’s exactly how supervised learning works. You feed the algorithm examples where you <em>already know</em> the right answer (we call these “labels”). The algorithm learns the patterns and eventually can make predictions on new, unseen data.</p>

<p>Examples you’ve probably used:</p>
<ul>
  <li>Email spam filters (learns from emails you’ve marked as spam)</li>
  <li>Netflix recommendations (learns from movies you’ve rated)</li>
  <li>Voice assistants recognizing your speech</li>
</ul>

<p><strong>Unsupervised learning</strong> is more like being dropped into a library with no guidance. Nobody tells you what’s important—you just start noticing things yourself. “Hey, these books all have similar covers. And these ones seem to be about the same topics.”</p>

<p>The algorithm finds patterns and groupings <em>without</em> being told what to look for.</p>

<p>Real-world examples:</p>
<ul>
  <li>Customer segmentation (grouping shoppers by behavior without predefined categories)</li>
  <li>Anomaly detection (finding weird transactions in your bank account)</li>
  <li>Organizing photo libraries by similar faces</li>
</ul>

<p>Here’s the key difference I tell people: <strong>supervised learning is like learning with a teacher, unsupervised learning is like being a detective.</strong></p>

<h2 id="gradient-descent-finding-the-bottom-of-a-valley-blindfolded">Gradient Descent: Finding the Bottom of a Valley (Blindfolded)</h2>

<p>This one is my favorite to explain because once it clicks, you never forget it.</p>

<p>Imagine you’re dropped somewhere on a hilly landscape, blindfolded. Your goal? Get to the lowest point in the valley. You can’t see anything, but you <em>can</em> feel the slope under your feet.</p>

<p>What would you do?</p>

<p>You’d probably:</p>
<ol>
  <li>Feel which direction slopes downward</li>
  <li>Take a step in that direction</li>
  <li>Feel the slope again</li>
  <li>Repeat until you’re not going down anymore</li>
</ol>

<p>That’s gradient descent. Seriously, that’s it.</p>

<p>In ML terms:</p>
<ul>
  <li>The “landscape” is your loss function (how wrong your predictions are)</li>
  <li>The “slope” is the gradient (mathematical direction of steepest increase)</li>
  <li>Taking a step is updating your model’s parameters</li>
  <li>“Downward” means reducing the error</li>
</ul>

<p><strong>The learning rate</strong> (we’ll talk more about this later) is basically how big your steps are. Too big? You might overshoot the bottom and climb up the other side. Too small? You’ll take forever to get anywhere.</p>

<p>I sometimes picture a marble rolling down a bowl. It naturally finds the bottom—that’s the intuition behind gradient descent.</p>

<h3 id="a-quick-thought-experiment">A Quick Thought Experiment</h3>

<p>Think about tuning the temperature in your shower. You start too cold, so you turn it up. Now it’s too hot, so you turn it down a bit. You keep adjusting until it feels <em>just right</em>.</p>

<p>That’s gradient descent in action—you’re making small adjustments based on feedback (too hot/too cold) until you minimize your discomfort. The “gradient” is the feedback telling you which way to turn.</p>

<h2 id="loss-function-your-models-report-card">Loss Function: Your Model’s Report Card</h2>

<p>Here’s a concept that confused me for the longest time because nobody explained <em>why</em> we need it.</p>

<p>A loss function (sometimes called objective function or cost function—same thing, different names) answers one simple question: <strong>How wrong is my model right now?</strong></p>

<p>Think of it like this. You’re playing darts:</p>
<ul>
  <li>Your “model” is your throwing technique</li>
  <li>Each throw is a prediction</li>
  <li>The bullseye is the actual correct answer</li>
  <li>The loss function measures how far from the bullseye you landed</li>
</ul>

<p>If you hit the bullseye, your loss is zero—perfect prediction. The further away you land, the higher the loss.</p>

<p>Different problems need different loss functions. Missing by 2 inches might matter a lot for brain surgery, but not so much for horseshoes. That’s why we have:</p>

<ul>
  <li><strong>Mean Squared Error</strong>: Penalizes big mistakes heavily (squares the errors)</li>
  <li><strong>Absolute Error</strong>: Treats all mistakes equally</li>
  <li><strong>Cross-Entropy</strong>: Used when you’re classifying things (spam vs. not spam)</li>
</ul>

<p>The key insight: <strong>gradient descent uses the loss function to figure out which way is “down.”</strong> Without a loss function, the algorithm wouldn’t know if it’s getting better or worse.</p>

<h2 id="regularization-teaching-your-model-to-not-be-a-know-it-all">Regularization: Teaching Your Model to Not Be a Know-It-All</h2>

<p>This one’s subtle but incredibly important.</p>

<p>Imagine a student who memorizes every single word in the textbook for an exam. They can recite any page perfectly. But then the exam asks them to apply the concepts to a <em>new</em> problem they’ve never seen—and they freeze.</p>

<p>That’s overfitting. The model learned the training data <em>too well</em>, including all the noise and random quirks that won’t appear in new data.</p>

<p><strong>Regularization</strong> is basically telling your model: “Hey, don’t get too fancy. Keep things simple.”</p>

<p>Here’s my favorite analogy. You know how some people pack for a trip and bring <em>everything</em>? “I might need this umbrella, and these three types of shoes, and this backup phone charger…” Their suitcase weighs 50 kg.</p>

<p>Regularization is like telling them: “Every item has a cost. Only bring what you really need.”</p>

<p>In ML, regularization adds a penalty for complexity. The model can still learn complex patterns, but it has to “pay” for them. This forces it to focus on patterns that genuinely matter.</p>

<p><strong>L1 regularization</strong> (Lasso): “Each extra feature costs a flat fee” → tends to eliminate useless features entirely</p>

<p><strong>L2 regularization</strong> (Ridge): “Each extra feature costs proportionally to how much you use it” → shrinks feature importance but rarely eliminates</p>

<p>The result? A model that doesn’t memorize—it actually <em>learns</em>.</p>

<h2 id="generalization-the-whole-point-of-all-this">Generalization: The Whole Point of All This</h2>

<p>Here’s something that took me way too long to internalize: <strong>We don’t care how well a model performs on data it’s already seen.</strong></p>

<p>Let me say that again because it’s that important.</p>

<p>If I show you 100 photos and ask you to memorize which ones have cats, you could get 100% accuracy on those specific photos. But that doesn’t mean you <em>understand</em> what a cat looks like. Can you recognize a cat in a photo you’ve never seen?</p>

<p>That’s generalization—performing well on new, unseen data.</p>

<p>Everything we do in ML is in service of this goal:</p>
<ul>
  <li>We split data into training and test sets (to check if we’re actually learning)</li>
  <li>We use regularization (to prevent memorizing)</li>
  <li>We tune hyperparameters carefully (to find the sweet spot)</li>
</ul>

<p>I like to think of it like learning to cook. You don’t want to only make your mom’s exact recipe perfectly. You want to understand cooking well enough to adapt when you’re missing an ingredient or trying a new dish.</p>

<p><strong>A model that can’t generalize is basically useless</strong>—no matter how impressive its training accuracy looks.</p>

<h2 id="hyperparameter-tuning-finding-the-perfect-recipe">Hyperparameter Tuning: Finding the Perfect Recipe</h2>

<p>Okay, this is where it gets practical.</p>

<p>Hyperparameters are settings you choose <em>before</em> training starts. They’re not learned from the data—you have to pick them yourself.</p>

<p>Think of it like baking a cake:</p>
<ul>
  <li>The <strong>ingredients</strong> are your data</li>
  <li>The <strong>recipe instructions</strong> are your model architecture</li>
  <li>The <strong>oven temperature and baking time</strong> are your hyperparameters</li>
</ul>

<p>You can have the best ingredients and recipe in the world, but bake at the wrong temperature? Disaster.</p>

<p>Common hyperparameters you’ll encounter:</p>
<ul>
  <li><strong>Learning rate</strong>: How big steps to take during gradient descent (too high = chaotic, too low = takes forever)</li>
  <li><strong>Number of layers/neurons</strong>: How complex the model can be</li>
  <li><strong>Regularization strength</strong>: How much to penalize complexity</li>
  <li><strong>Batch size</strong>: How many examples to look at before adjusting</li>
</ul>

<p><strong>How do you find good hyperparameters?</strong></p>

<p>Honestly? Trial and error, but systematic.</p>

<ol>
  <li><strong>Grid search</strong>: Try every combination (slow but thorough)</li>
  <li><strong>Random search</strong>: Randomly sample combinations (surprisingly effective)</li>
  <li><strong>Bayesian optimization</strong>: Smart guessing based on previous results</li>
</ol>

<p>Here’s what nobody tells beginners: there’s no “correct” answer. Different problems need different settings. It’s genuinely an art as much as a science.</p>

<p>My personal approach: start with defaults from whatever library you’re using, train a baseline model, then tweak one thing at a time. Keep notes on what works. Over time, you develop intuition.</p>

<h2 id="putting-it-all-together">Putting It All Together</h2>

<p>Let me tie everything together with a story.</p>

<p>Imagine you’re teaching someone to play chess:</p>

<ol>
  <li>
    <p><strong>Supervised learning</strong>: You show them famous games with commentary. “This move was good because…” They learn from examples with known outcomes.</p>
  </li>
  <li>
    <p><strong>Loss function</strong>: After each game they play, you give them a score based on their performance.</p>
  </li>
  <li>
    <p><strong>Gradient descent</strong>: They adjust their strategy bit by bit, based on what worked and what didn’t.</p>
  </li>
  <li>
    <p><strong>Regularization</strong>: You tell them not to memorize specific openings, but to understand <em>principles</em> that apply broadly.</p>
  </li>
  <li>
    <p><strong>Hyperparameters</strong>: You decide how many games to play per day, how much feedback to give, how long to think per move—settings that affect learning but aren’t the learning itself.</p>
  </li>
  <li>
    <p><strong>Generalization</strong>: The goal isn’t to replay the famous games perfectly—it’s to beat opponents they’ve never faced before.</p>
  </li>
</ol>

<p>That’s machine learning in a nutshell.</p>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>I’ve been working with ML for years now, and honestly, these fundamentals still come up every single day. When a model isn’t working, nine times out of ten, it’s one of these basics that’s off.</p>

<p>The fancy stuff—transformers, diffusion models, whatever’s trending on Twitter this week—it’s all built on these foundations. Nail these concepts, and the rest becomes much more approachable.</p>

<p>Got questions? I probably glossed over something that deserves more attention. Drop me a message—I genuinely enjoy these conversations.</p>

<hr />

<p><em>This post is part of a series where I try to explain technical concepts without the jargon. If you found this helpful, you might also like my posts on <a href="/2025/09/18/distributed-rate-limiter-spring-boot-redis.html">distributed rate limiting</a>.</em></p>]]></content><author><name>Burhanettin Nacar</name></author><category term="Machine Learning" /><category term="AI" /><category term="Fundamentals" /><category term="Beginner Friendly" /><category term="Data Science" /><summary type="html"><![CDATA[ML jargon can feel like a foreign language. Here's my attempt to explain supervised vs unsupervised learning, gradient descent, loss functions, regularization, and hyperparameter tuning using everyday examples that actually make sense.]]></summary></entry><entry><title type="html">I Finally Understand Machine Learning Models (And You Can Too)</title><link href="https://bnacar.dev/2025/11/20/machine-learning-models-explained-simply.html" rel="alternate" type="text/html" title="I Finally Understand Machine Learning Models (And You Can Too)" /><published>2025-11-20T00:00:00+00:00</published><updated>2025-11-20T00:00:00+00:00</updated><id>https://bnacar.dev/2025/11/20/machine-learning-models-explained-simply</id><content type="html" xml:base="https://bnacar.dev/2025/11/20/machine-learning-models-explained-simply.html"><![CDATA[<p>I remember the first time someone tried to explain machine learning to me. They started drawing hyperplanes on a whiteboard and talking about “gradient descent” within the first two minutes. I nodded along, pretending to understand, but honestly? I was completely lost.</p>

<p>Here’s what I wish someone had told me back then: these ML models are just pattern-matching techniques inspired by how we naturally solve problems. No magic, just math doing what our brains do every day. Let me show you what I mean.</p>

<h2 id="clustering---when-your-computer-organizes-your-mess">Clustering - When Your Computer Organizes Your Mess</h2>

<p>We all have that one folder on our desktop called “Stuff” or “To Sort”, right? Clustering is basically the algorithm that says “let me handle that mess for you.”</p>

<p>It looks at your 1,000 random songs and groups them without you labeling anything. The upbeat workout tracks end up together, chill piano music forms its own group, rock songs cluster separately. The algorithm just finds patterns in tempo, instruments, and style.</p>

<p>I love this because it’s unsupervised - you don’t have to teach it what “rock” means. It just figures it out. That’s the same tech behind how Netflix groups movies or how stores figure out which products are usually bought together.</p>

<h2 id="logistic-regression---the-yesno-decision-maker">Logistic Regression - The Yes/No Decision Maker</h2>

<p>First off, terrible name. It sounds like something from a supply chain meeting. But it’s actually super simple: it’s the “Yes or No” machine.</p>

<p>Think about deciding whether to bring an umbrella. You check the clouds (80% coverage), humidity (super high at 95%), temperature, wind speed… Your brain weighs all this and concludes: “Yeah, bring the umbrella.” That’s exactly what logistic regression does, except it gives you a probability like 85%.</p>

<p>Your email spam filter? Logistic regression. Loan approvals? Same thing. It’s everywhere because most real-world problems boil down to binary choices.</p>

<hr />

<h2 id="decision-trees---the-flowchart-algorithm">Decision Trees - The Flowchart Algorithm</h2>

<p>Remember those “Choose Your Own Adventure” books from when we were kids? Decision trees are exactly that, just with math.</p>

<p>You’re hungry. Very hungry? If yes, do you have 30 minutes? Yes means cook a meal, no means order takeout. Not very hungry? Want something healthy? And so on. Each answer branches to the next question until you land on a decision.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hungry?
├─ Starving → Got time?
│   ├─ Yes → Cook
│   └─ No → Order pizza
└─ Meh → Healthy?
    ├─ Yes → Salad
    └─ No → Snacks
</code></pre></div></div>

<p>Those annoying customer service chatbots use these. So do credit scoring systems. Honestly, they’re kind of like how we naturally make decisions anyway.</p>

<hr />

<h2 id="collaborative-filtering---your-digital-friends-recommendation">Collaborative Filtering - Your Digital Friend’s Recommendation</h2>

<p>This is basically digital peer pressure. You know the “people who bought this also bought that” section on Amazon? That’s this guy.</p>

<p>Here’s the idea: you and I both binge-watched The Office and love Thai food. I just discovered a new podcast and rated it 5 stars. The algorithm figures “hey, we have similar taste, so you’ll probably like this podcast too.”</p>

<p>It’s crowdsourcing recommendations. Spotify’s Discover Weekly? Same concept - finding people with your music taste and suggesting what they’re listening to. Sometimes it’s creepy accurate, sometimes it completely misses (no, Spotify, I don’t want to listen to polka just because I clicked one weird link). But when it works, it works well.</p>

<hr />

<h2 id="neural-networks---the-brain-inspired-stuff">Neural Networks - The Brain-Inspired Stuff</h2>

<p>Okay, this is where things get a little sci-fi. Neural networks are loosely inspired by how our brains work, but don’t let that scare you.</p>

<h3 id="feed-forward-networks-one-way-street">Feed-Forward Networks: One-Way Street</h3>

<p>Think assembly line. Raw materials go in one end, each station does its thing, finished product comes out the other end. No going backwards.</p>

<p>Input → Hidden layers doing math → Output. That’s it.</p>

<p>These are the OG neural networks. They’re used for basic stuff like reading handwritten zip codes on envelopes or predicting house prices. Nothing fancy, but they work.</p>

<hr />

<h3 id="recurrent-neural-networks-rnns-the-one-with-memory">Recurrent Neural Networks (RNNs): The One With Memory</h3>

<p>Feed-forward networks have the memory of a goldfish—they process one thing and immediately forget it. RNNs are different; they actually remember context.</p>

<p>When you read “Sarah went to the store. She bought eggs. Then she went home,” you know “she” refers to Sarah because you remember the first sentence. RNNs do the same thing - they carry context forward.</p>

<p>This is why your phone’s autocomplete works. It doesn’t just look at the current word, it remembers what you’ve been typing. Voice assistants use these too. The problem? RNNs are kind of forgetful with long sequences. They’re like that friend who remembers the beginning of a story but gets fuzzy on the middle parts.</p>

<hr />

<h3 id="convolutional-neural-networks-cnns-the-image-expert">Convolutional Neural Networks (CNNs): The Image Expert</h3>

<p>If you’ve ever wondered how your phone knows which photo has your dog in it, this is the answer.</p>

<p>They look at images in layers, starting small and building up:</p>
<ul>
  <li>First layer: detects edges and simple patterns</li>
  <li>Next layer: combines those into shapes</li>
  <li>Next: combines shapes into features like “eye” or “nose”</li>
  <li>Final layer: “Oh, this is a face!”</li>
</ul>

<p>It’s honestly similar to how we look at pictures. You don’t immediately see “John” - your brain processes edges, then features, then puts it together. CNNs copy that approach.</p>

<p>Self-driving cars use these to spot pedestrians. Doctors use them to find tumors in X-rays. They’re really good at anything visual.</p>

<hr />

<h3 id="transformers-the-new-hotness">Transformers: The New Hotness</h3>

<p>This is the big one. The reason everyone is freaking out about AI right now? It’s mostly because of these guys.</p>

<p>Transformers are what powers ChatGPT, Google Translate, and basically every modern AI that handles language.</p>

<p>The breakthrough? Attention mechanism. Instead of processing words one-by-one like RNNs, transformers look at everything simultaneously and figure out what’s important.</p>

<p>Imagine you’re at a party with multiple conversations happening. RNNs would eavesdrop on one conversation at a time. Transformers have super-hearing - they catch all conversations at once AND figure out that Person A in the kitchen is finishing Person B’s story from 20 minutes ago.</p>

<p>This “attention” to the whole context is why modern translation is so good. The model sees the entire sentence before deciding how to translate each word. Context matters, and transformers are phenomenal at context.</p>

<p>They’re also why we can have these scary-good AI writing tools now. The compute cost is high, but the results speak for themselves.</p>

<hr />

<h2 id="why-this-matters">Why This Matters</h2>

<p>It’s easy to get lost in the hype, but at the end of the day, these are just tools. Really smart tools, but tools nonetheless.</p>

<p>Your morning routine probably involves face unlock (CNN), checking if there’s traffic (RNNs predicting patterns), scrolling a personalized news feed (collaborative filtering). During lunch, your spam filter (logistic regression) is working. By evening, Netflix is recommending shows (collaborative filtering + neural networks), and you’re asking Siri something (transformers + RNNs).</p>

<p>The trick is knowing which tool fits which problem. You wouldn’t use a hammer to tighten a screw, right? Same deal here:</p>

<ul>
  <li>Got images? CNN is your friend</li>
  <li>Dealing with sequences or text? Transformers or RNNs</li>
  <li>Need simple yes/no predictions? Logistic regression</li>
  <li>Want to find natural groupings? Clustering</li>
  <li>Building a recommendation system? Collaborative filtering</li>
</ul>

<p>I still remember being completely overwhelmed by ML terminology. But once you strip away the jargon, these are just formalized versions of how we already think about problems. The math gets complicated, sure, but the core ideas? They’re intuitive.</p>

<p>If you want to dive deeper, pick one model and build something small with it. Trust me, there’s no better teacher than breaking things and fixing them yourself.</p>]]></content><author><name>Burhanettin Nacar</name></author><category term="Machine Learning" /><category term="AI" /><category term="Tutorial" /><summary type="html"><![CDATA[Confused by machine learning models? This guide breaks down common algorithms like Clustering, Logistic Regression, Decision Trees, and Neural Networks using simple, real-world analogies.]]></summary></entry><entry><title type="html">The Hidden Complexity of Distributed Rate Limiting: Lessons from Building 5 Algorithms</title><link href="https://bnacar.dev/2025/10/23/hidden-complexity-of-rate-limiting.html" rel="alternate" type="text/html" title="The Hidden Complexity of Distributed Rate Limiting: Lessons from Building 5 Algorithms" /><published>2025-10-23T20:00:00+00:00</published><updated>2025-10-23T20:00:00+00:00</updated><id>https://bnacar.dev/2025/10/23/hidden-complexity-of-rate-limiting</id><content type="html" xml:base="https://bnacar.dev/2025/10/23/hidden-complexity-of-rate-limiting.html"><![CDATA[<p>I spent the last few months building a distributed rate limiter, and honestly, I underestimated how nuanced this problem is. What started as “just implement token bucket with Redis” turned into a deep dive into algorithm trade-offs, Redis optimization, and some interesting architectural decisions I’d love your feedback on.</p>

<h2 id="the-problem-that-started-it-all">The Problem That Started It All</h2>

<p>Like many of you, I needed rate limiting across multiple service instances. The typical in-memory solutions don’t work when you have 5 instances behind a load balancer - suddenly your 100 req/min limit becomes 500 req/min.</p>

<p>My first thought: “Just use Redis!” Turns out, that’s where the real complexity begins.</p>

<h2 id="algorithm-1-token-bucket---the-obvious-choice-until-it-wasnt">Algorithm #1: Token Bucket - The Obvious Choice (Until It Wasn’t)</h2>

<p>Token bucket is everyone’s first choice, right? Tokens refill at a constant rate, requests consume tokens, simple math.</p>

<p>Here’s where it got interesting: <strong>When do you refill?</strong></p>

<p><strong>Naive approach</strong> (what I tried first):</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Check on every request - seemed elegant</span>
<span class="n">currentTokens</span> <span class="o">=</span> <span class="n">min</span><span class="o">(</span><span class="n">capacity</span><span class="o">,</span> <span class="n">lastTokens</span> <span class="o">+</span> <span class="o">(</span><span class="n">now</span> <span class="o">-</span> <span class="n">lastRefill</span><span class="o">)</span> <span class="o">*</span> <span class="n">refillRate</span><span class="o">)</span>
</code></pre></div></div>

<p><strong>Problem</strong>: Race conditions everywhere. Two requests at the exact same millisecond? You’re in trouble.</p>

<p><strong>Solution</strong>: Lua scripts in Redis for atomic operations. But here’s the catch - Lua scripts have a size limit, and complex refill logic bloats quickly.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- This runs atomically in Redis</span>
<span class="kd">local</span> <span class="n">tokens</span> <span class="o">=</span> <span class="n">redis</span><span class="p">.</span><span class="n">call</span><span class="p">(</span><span class="s1">'HGET'</span><span class="p">,</span> <span class="n">KEYS</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="s1">'tokens'</span><span class="p">)</span>
<span class="kd">local</span> <span class="n">last_refill</span> <span class="o">=</span> <span class="n">redis</span><span class="p">.</span><span class="n">call</span><span class="p">(</span><span class="s1">'HGET'</span><span class="p">,</span> <span class="n">KEYS</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="s1">'lastRefill'</span><span class="p">)</span>
<span class="c1">-- ... refill calculation ...</span>
<span class="n">redis</span><span class="p">.</span><span class="n">call</span><span class="p">(</span><span class="s1">'HSET'</span><span class="p">,</span> <span class="n">KEYS</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="s1">'tokens'</span><span class="p">,</span> <span class="n">new_tokens</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>Question for the community</strong>: Is there a better pattern than Lua scripts for distributed atomic operations? I looked at RedLock but it felt too heavy for this use case.</p>

<h2 id="algorithm-2-sliding-window---the-precision-trade-off">Algorithm #2: Sliding Window - The Precision Trade-off</h2>

<p>Token bucket has a problem: boundary issues. Fire 100 requests at 11:59:59, then 100 more at 12:00:01, and you’ve “technically” stayed within limits but hammered the system with 200 requests in 2 seconds.</p>

<p><strong>Sliding window</strong> fixes this by tracking requests in overlapping time windows. But the memory cost is brutal.</p>

<p><strong>The trade-off I faced</strong>:</p>
<ul>
  <li><strong>Store every request timestamp</strong>: Accurate but O(n) memory per key</li>
  <li><strong>Fixed time buckets</strong>: Memory efficient but brings back boundary issues</li>
  <li><strong>Hybrid approach</strong>: Store request counts in sub-windows</li>
</ul>

<p>I went with the hybrid - store counts in 10-second buckets, interpolate between them:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Simplified concept</span>
<span class="kt">double</span> <span class="n">weight</span> <span class="o">=</span> <span class="o">(</span><span class="n">now</span> <span class="o">-</span> <span class="n">windowStart</span><span class="o">)</span> <span class="o">/</span> <span class="n">windowSize</span><span class="o">;</span>
<span class="n">count</span> <span class="o">=</span> <span class="n">pastBucketCount</span> <span class="o">*</span> <span class="o">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">weight</span><span class="o">)</span> <span class="o">+</span> <span class="n">currentBucketCount</span> <span class="o">*</span> <span class="n">weight</span><span class="o">;</span>
</code></pre></div></div>

<p><strong>Is this a good compromise?</strong> I’m seeing ~5% error margin compared to true sliding window. Worth the 90% memory savings?</p>

<h2 id="algorithm-3-fixed-window---when-good-enough-actually-is">Algorithm #3: Fixed Window - When “Good Enough” Actually Is</h2>

<p>Fixed window is the simplest: reset counter every N seconds. Everyone hates it because of boundary issues, but hear me out…</p>

<p><strong>When it’s actually perfect</strong>:</p>
<ul>
  <li>High-scale scenarios where you can tolerate boundary spikes</li>
  <li>Background job processing (who cares about exact timing?)</li>
  <li>Internal service-to-service limits</li>
</ul>

<p>I implemented it in ~20 lines of code (vs 200+ for sliding window). The memory usage? Nearly zero - just one counter per key with TTL.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- That's literally it</span>
<span class="kd">local</span> <span class="n">count</span> <span class="o">=</span> <span class="n">redis</span><span class="p">.</span><span class="n">call</span><span class="p">(</span><span class="s1">'INCR'</span><span class="p">,</span> <span class="n">KEYS</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
<span class="k">if</span> <span class="n">count</span> <span class="o">==</span> <span class="mi">1</span> <span class="k">then</span>
    <span class="n">redis</span><span class="p">.</span><span class="n">call</span><span class="p">(</span><span class="s1">'EXPIRE'</span><span class="p">,</span> <span class="n">KEYS</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">window_size</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">return</span> <span class="n">count</span>
</code></pre></div></div>

<p><strong>Question</strong>: Why do we over-engineer rate limiting for internal services? Fixed window + generous limits seems fine for 90% of internal use cases.</p>

<h2 id="algorithm-4-leaky-bucket---traffic-shapings-best-friend">Algorithm #4: Leaky Bucket - Traffic Shaping’s Best Friend</h2>

<p>Here’s where I realized algorithm choice really matters. We had a service calling a legacy system that could only handle 10 requests/second - not 9, not 11, exactly 10.</p>

<p>Token bucket? Allows bursts. Sliding window? Still has variance. <strong>Leaky bucket</strong> processes requests at a constant rate, queuing the rest.</p>

<p><strong>The implementation challenge</strong>: Simulating a queue in Redis without actual queue semantics.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Conceptually: when will the bucket leak enough for this request?</span>
<span class="n">nextAvailableTime</span> <span class="o">=</span> <span class="n">max</span><span class="o">(</span><span class="n">now</span><span class="o">,</span> <span class="n">lastLeakTime</span> <span class="o">+</span> <span class="o">(</span><span class="n">queueSize</span> <span class="o">/</span> <span class="n">leakRate</span><span class="o">))</span>
<span class="k">if</span> <span class="o">(</span><span class="n">nextAvailableTime</span> <span class="o">-</span> <span class="n">now</span><span class="o">)</span> <span class="o">&gt;</span> <span class="n">maxWaitTime</span> <span class="o">{</span>
    <span class="n">reject</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>This is where I’d love opinions</strong>: Should a rate limiter handle queuing at all? Or just accept/reject and let the client retry? I implemented both modes, but I’m not sure the queuing mode is worth the complexity.</p>

<h2 id="algorithm-5-composite---because-reality-is-complicated">Algorithm #5: Composite - Because Reality Is Complicated</h2>

<p>Real-world scenario that broke my elegant single-algorithm design:</p>

<p><em>“We need to limit API calls to 1000/hour, BUT also limit bandwidth to 10MB/hour, AND ensure no single user exceeds 100 calls in any 5-minute window for compliance.”</em></p>

<p>One algorithm can’t handle this. I needed to combine multiple algorithms.</p>

<p><strong>The architecture decision</strong>: How do you combine rate limiters?</p>

<p><strong>Option 1: Sequential checks</strong> (what I built first)</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="o">(!</span><span class="n">checkApiCallLimit</span><span class="o">())</span> <span class="k">return</span> <span class="n">reject</span><span class="o">(</span><span class="s">"API calls"</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">checkBandwidthLimit</span><span class="o">())</span> <span class="k">return</span> <span class="n">reject</span><span class="o">(</span><span class="s">"Bandwidth"</span><span class="o">);</span>  
<span class="k">if</span> <span class="o">(!</span><span class="n">checkComplianceLimit</span><span class="o">())</span> <span class="k">return</span> <span class="n">reject</span><span class="o">(</span><span class="s">"Compliance"</span><span class="o">);</span>
<span class="k">return</span> <span class="nf">allow</span><span class="o">();</span>
</code></pre></div></div>

<p>Simple, but you’re making 3 Redis calls. Latency stacks.</p>

<p><strong>Option 2: Parallel checks</strong></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Boolean</span><span class="o">&gt;</span> <span class="n">apiCheck</span> <span class="o">=</span> <span class="n">checkApiCallLimit</span><span class="o">();</span>
<span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Boolean</span><span class="o">&gt;</span> <span class="n">bandwidthCheck</span> <span class="o">=</span> <span class="n">checkBandwidthLimit</span><span class="o">();</span>
<span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Boolean</span><span class="o">&gt;</span> <span class="n">complianceCheck</span> <span class="o">=</span> <span class="n">checkComplianceLimit</span><span class="o">();</span>

<span class="k">return</span> <span class="n">apiCheck</span><span class="o">.</span><span class="na">get</span><span class="o">()</span> <span class="o">&amp;&amp;</span> <span class="n">bandwidthCheck</span><span class="o">.</span><span class="na">get</span><span class="o">()</span> <span class="o">&amp;&amp;</span> <span class="n">complianceCheck</span><span class="o">.</span><span class="na">get</span><span class="o">();</span>
</code></pre></div></div>

<p>Better latency, but now you’re consuming tokens from limits you might not even need to check. If API limit fails, why decrement bandwidth?</p>

<p><strong>Option 3: Smart short-circuit</strong>
Check cheapest limits first (like fixed window), only proceed if they pass. But which order? Do you hardcode it? Make it configurable?</p>

<p><strong>I went with configurable combination logic</strong>:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">ALL_MUST_PASS</code>: AND logic, fail-fast</li>
  <li><code class="language-plaintext highlighter-rouge">WEIGHTED_AVERAGE</code>: Each limit gets a score, combined threshold</li>
  <li><code class="language-plaintext highlighter-rouge">HIERARCHICAL</code>: User limits before tenant limits before global</li>
  <li><code class="language-plaintext highlighter-rouge">PRIORITY_BASED</code>: High-priority limits checked first</li>
</ul>

<p><strong>Is this over-engineered?</strong> Part of me thinks “just do AND and call it a day.” But the flexibility has been useful in testing.</p>

<h2 id="the-redis-optimization-rabbit-hole">The Redis Optimization Rabbit Hole</h2>

<p>Let me share the performance journey, because this is where things got interesting.</p>

<p><strong>Initial naive implementation</strong>: ~200 req/s per instance
<strong>After optimizations</strong>: 50,000+ req/s per instance</p>

<p>What changed?</p>

<h3 id="1-connection-pooling-drama">1. Connection Pooling Drama</h3>

<p>First mistake: Creating a new Redis connection per request. Rookie error, but the performance impact was insane.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Don't do this</span>
<span class="nc">Jedis</span> <span class="n">jedis</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Jedis</span><span class="o">(</span><span class="s">"localhost"</span><span class="o">);</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">close</span><span class="o">();</span>  <span class="c1">// Connection overhead killed us</span>
</code></pre></div></div>

<p>Switched to Lettuce with proper pooling. But then…</p>

<p><strong>The connection pool sizing problem</strong>: Too small = bottleneck. Too large = connection overhead. I landed on:</p>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.data.redis.lettuce.pool.max-active</span><span class="p">=</span><span class="s">32</span>
<span class="py">spring.data.redis.lettuce.pool.max-idle</span><span class="p">=</span><span class="s">8</span>
</code></pre></div></div>

<p><strong>How did you arrive at these numbers?</strong> Load testing. But I’m curious how others size their Redis pools.</p>

<h3 id="2-lua-script-optimization">2. Lua Script Optimization</h3>

<p>My first Lua script was 150 lines. It was slow.</p>

<p><strong>Key optimization</strong>: Pre-calculate as much as possible in Java, keep Lua minimal.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Before: Doing math in Lua</span>
<span class="kd">local</span> <span class="n">refill_amount</span> <span class="o">=</span> <span class="p">(</span><span class="n">now</span> <span class="o">-</span> <span class="n">last_time</span><span class="p">)</span> <span class="o">*</span> <span class="n">rate</span> <span class="o">/</span> <span class="mi">1000</span>

<span class="c1">-- After: Pass pre-calculated value from Java</span>
<span class="kd">local</span> <span class="n">refill_amount</span> <span class="o">=</span> <span class="n">ARGV</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>  <span class="c1">-- Already calculated</span>
</code></pre></div></div>

<p>Sounds obvious but cut latency by 40%.</p>

<h3 id="3-the-ttl-revelation">3. The TTL Revelation</h3>

<p>Memory leak alert: I wasn’t setting TTLs on rate limit keys. After a week in production, Redis was using 10GB for ~100K active users.</p>

<p><strong>The insight</strong>: Most rate limit keys are temporary. User stops hitting your API? That key should expire.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Always set expiry</span>
<span class="n">redis</span><span class="p">.</span><span class="n">call</span><span class="p">(</span><span class="s1">'EXPIRE'</span><span class="p">,</span> <span class="n">KEYS</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="mi">3600</span><span class="p">)</span>  <span class="c1">-- 1 hour</span>
</code></pre></div></div>

<p>This one change dropped memory usage by 95%. Sometimes the simple solutions are the best.</p>

<h2 id="architectural-decisions-im-still-questioning">Architectural Decisions I’m Still Questioning</h2>

<h3 id="decision-1-fail-open-vs-fail-closed">Decision 1: Fail-Open vs Fail-Closed</h3>

<p>When Redis goes down (and it will), what do you do?</p>

<p><strong>Fail-Open</strong> (what I chose): Allow all requests</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nf">checkRedisRateLimit</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">RedisException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">log</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"Redis down, failing open"</span><span class="o">);</span>
    <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>  <span class="c1">// Allow request</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>Reasoning</strong>: Rate limiting is protective, not critical. Better to briefly over-serve than to have a Redis outage take down your entire API.</p>

<p><strong>But</strong>: This can be abused. If someone knows your Redis is down, they can flood you.</p>

<p><strong>Alternative I considered</strong>: In-memory fallback with local limits. Problem: Defeats the “distributed” part. With 5 instances, you 5x your limits again.</p>

<p><strong>What would you do?</strong> I’m genuinely torn on this one.</p>

<h3 id="decision-2-synchronous-vs-asynchronous-checks">Decision 2: Synchronous vs Asynchronous Checks</h3>

<p>Rate limit checks are in the hot path. Every microsecond matters.</p>

<p><strong>Synchronous</strong> (current approach):</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="o">(!</span><span class="n">rateLimiter</span><span class="o">.</span><span class="na">isAllowed</span><span class="o">(</span><span class="n">request</span><span class="o">))</span> <span class="o">{</span>
    <span class="k">return</span> <span class="no">HTTP_429</span><span class="o">;</span>
<span class="o">}</span>
<span class="n">processRequest</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
</code></pre></div></div>

<p>Clean, simple, blocks until decision is made.</p>

<p><strong>Asynchronous alternative</strong>:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Boolean</span><span class="o">&gt;</span> <span class="n">limitCheck</span> <span class="o">=</span> <span class="n">rateLimiter</span><span class="o">.</span><span class="na">isAllowedAsync</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
<span class="c1">// ... do other work ...</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">limitCheck</span><span class="o">.</span><span class="na">get</span><span class="o">())</span> <span class="k">return</span> <span class="no">HTTP_429</span><span class="o">;</span>
</code></pre></div></div>

<p>Could overlap with other I/O. But adds complexity.</p>

<p><strong>My take</strong>: For rate limiting, clarity &gt; async performance gains. But I’ve seen arguments both ways.</p>

<h3 id="decision-3-client-side-vs-server-side-token-tracking">Decision 3: Client-Side vs Server-Side Token Tracking</h3>

<p>Should clients know their token count?</p>

<p><strong>Current</strong>: Server returns remaining tokens</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"allowed"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"tokensRemaining"</span><span class="p">:</span><span class="w"> </span><span class="mi">42</span><span class="p">,</span><span class="w">
  </span><span class="nl">"resetTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2025-10-23T12:00:00Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Why</strong>: Clients can back off proactively, prevents wasted requests.</p>

<p><strong>Risk</strong>: Clients can game the system, optimize their behavior to stay just under limits.</p>

<p><strong>Alternative</strong>: Just return true/false. Simpler, but clients have to guess-and-check.</p>

<h2 id="testing-challenges-nobody-talks-about">Testing Challenges Nobody Talks About</h2>

<p>Unit testing a rate limiter is deceptively hard. How do you test time-based logic without <code class="language-plaintext highlighter-rouge">Thread.sleep()</code>?</p>

<h3 id="the-time-problem">The Time Problem</h3>

<p><strong>Bad approach</strong>:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">testRefill</span><span class="o">()</span> <span class="o">{</span>
    <span class="n">rateLimiter</span><span class="o">.</span><span class="na">consume</span><span class="o">(</span><span class="mi">10</span><span class="o">);</span>
    <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">1000</span><span class="o">);</span>  <span class="c1">// Wait for refill</span>
    <span class="n">assertTrue</span><span class="o">(</span><span class="n">rateLimiter</span><span class="o">.</span><span class="na">hasTokens</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Slow tests, flaky on CI, terrible developer experience.</p>

<p><strong>My solution</strong>: Inject a clock interface</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">RateLimiter</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Clock</span> <span class="n">clock</span><span class="o">;</span>  <span class="c1">// Can be mocked</span>
    
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">allow</span><span class="o">(</span><span class="nc">String</span> <span class="n">key</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">long</span> <span class="n">now</span> <span class="o">=</span> <span class="n">clock</span><span class="o">.</span><span class="na">millis</span><span class="o">();</span>
        <span class="c1">// ... rate limit logic ...</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// In tests</span>
<span class="nd">@Test</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">testRefill</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">MockClock</span> <span class="n">clock</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MockClock</span><span class="o">();</span>
    <span class="nc">RateLimiter</span> <span class="n">limiter</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RateLimiter</span><span class="o">(</span><span class="n">clock</span><span class="o">);</span>
    
    <span class="n">limiter</span><span class="o">.</span><span class="na">consume</span><span class="o">(</span><span class="mi">10</span><span class="o">);</span>
    <span class="n">clock</span><span class="o">.</span><span class="na">advance</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">1</span><span class="o">));</span>  <span class="c1">// Instant time travel!</span>
    <span class="n">assertTrue</span><span class="o">(</span><span class="n">limiter</span><span class="o">.</span><span class="na">hasTokens</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Tests run in milliseconds, fully deterministic.</p>

<h3 id="the-distributed-problem">The Distributed Problem</h3>

<p>How do you test distributed behavior without actually running distributed instances?</p>

<p>I ended up using <strong>Testcontainers</strong> to spin up real Redis instances:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Testcontainers</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DistributedRateLimiterTest</span> <span class="o">{</span>
    
    <span class="nd">@Container</span>
    <span class="kd">static</span> <span class="nc">GenericContainer</span> <span class="n">redis</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">GenericContainer</span><span class="o">(</span><span class="s">"redis:7-alpine"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">withExposedPorts</span><span class="o">(</span><span class="mi">6379</span><span class="o">);</span>
    
    <span class="nd">@Test</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">testMultipleInstances</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// Simulate multiple app instances</span>
        <span class="nc">RateLimiter</span> <span class="n">instance1</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RateLimiter</span><span class="o">(</span><span class="n">redis</span><span class="o">.</span><span class="na">getHost</span><span class="o">(),</span> <span class="n">redis</span><span class="o">.</span><span class="na">getPort</span><span class="o">());</span>
        <span class="nc">RateLimiter</span> <span class="n">instance2</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RateLimiter</span><span class="o">(</span><span class="n">redis</span><span class="o">.</span><span class="na">getHost</span><span class="o">(),</span> <span class="n">redis</span><span class="o">.</span><span class="na">getPort</span><span class="o">());</span>
        
        <span class="c1">// Both instances should share state</span>
        <span class="n">instance1</span><span class="o">.</span><span class="na">consume</span><span class="o">(</span><span class="mi">5</span><span class="o">);</span>
        <span class="n">assertEquals</span><span class="o">(</span><span class="mi">5</span><span class="o">,</span> <span class="n">instance2</span><span class="o">.</span><span class="na">remainingTokens</span><span class="o">());</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Slower than unit tests, but caught a bunch of race conditions my mocked tests missed.</p>

<h2 id="what-id-do-differently">What I’d Do Differently</h2>

<p>Looking back, here’s what I’d change:</p>

<h3 id="1-start-with-fixed-window">1. Start with Fixed Window</h3>

<p>I spent weeks optimizing token bucket before realizing 80% of use cases don’t need that precision. Should’ve validated with fixed window first, optimized later.</p>

<h3 id="2-metrics-from-day-one">2. Metrics from Day One</h3>

<p>Added metrics after performance problems appeared. Should’ve had Prometheus integration from the start. You can’t optimize what you don’t measure.</p>

<h3 id="3-document-the-trade-offs">3. Document the Trade-offs</h3>

<p>I built ADRs (Architecture Decision Records) halfway through. Should’ve done them upfront. Writing “why we chose X over Y” forces you to think through edge cases.</p>

<h3 id="4-simpler-configuration">4. Simpler Configuration</h3>

<p>My config system has three layers: per-key, pattern-based, global defaults. Sounds flexible, but in practice it’s confusing. Simpler might be better.</p>

<h2 id="questions-for-the-community">Questions for the Community</h2>

<p>I’d genuinely love feedback on:</p>

<ol>
  <li>
    <p><strong>Algorithm choice</strong>: Am I overthinking this? Should I have just stuck with token bucket and called it done?</p>
  </li>
  <li>
    <p><strong>Redis patterns</strong>: Are there better patterns than Lua scripts for atomic distributed operations?</p>
  </li>
  <li>
    <p><strong>Fail-open vs fail-closed</strong>: What’s your take? Have you been burned by either approach?</p>
  </li>
  <li>
    <p><strong>Composite limiting</strong>: Over-engineered or actually useful? Do people need multi-dimensional rate limiting?</p>
  </li>
  <li>
    <p><strong>Client transparency</strong>: Should clients know their remaining quota, or is that information they shouldn’t have?</p>
  </li>
  <li>
    <p><strong>Testing approaches</strong>: How do you test distributed systems without the tests becoming a maintenance nightmare?</p>
  </li>
</ol>

<h2 id="the-code">The Code</h2>

<p>I open-sourced the whole thing: <a href="https://github.com/uppnrise/distributed-rate-limiter">github.com/uppnrise/distributed-rate-limiter</a></p>

<p><strong>Tech stack</strong>: Java 21, Spring Boot, Redis (Lettuce client), React dashboard for monitoring</p>

<p><strong>Performance</strong>: ~50K req/s per instance, P95 &lt; 2ms latency, ~100MB memory for 1M active limits</p>

<p>It’s MIT licensed, so use it however you want. But more importantly, I’d love to discuss the approaches and trade-offs.</p>

<h2 id="client-integration-example">Client Integration Example</h2>

<p>Since people usually ask, here’s how you’d use it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Simple POST request</span>
curl <span class="nt">-X</span> POST http://localhost:8080/api/ratelimit/check <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"key": "user:123", "tokensRequested": 1}'</span>

<span class="c"># Response</span>
<span class="o">{</span>
  <span class="s2">"allowed"</span>: <span class="nb">true</span>,
  <span class="s2">"tokensRemaining"</span>: 42,
  <span class="s2">"resetTime"</span>: <span class="s2">"2025-10-23T12:00:00Z"</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Simple REST API, works from any language. There’s also a React dashboard for visualizing what’s happening in real-time.</p>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>This project started as “I need distributed rate limiting” and turned into a deep exploration of distributed systems trade-offs.</p>

<p>The biggest lesson? <strong>There’s no “best” rate limiting algorithm.</strong> It depends entirely on your use case:</p>
<ul>
  <li>Token bucket for APIs with burst tolerance</li>
  <li>Sliding window for strict enforcement</li>
  <li>Fixed window for internal services and high scale</li>
  <li>Leaky bucket for traffic shaping</li>
  <li>Composite when reality is complicated</li>
</ul>

<p>I’d love to hear your experiences with rate limiting:</p>
<ul>
  <li>What approach do you use?</li>
  <li>Have you hit edge cases that broke your rate limiter?</li>
  <li>Are there patterns I’m missing?</li>
</ul>

<p>Drop a comment, and let’s discuss! Or check out the <a href="https://github.com/uppnrise/distributed-rate-limiter">repo</a> and open an issue if you spot something questionable in the implementation.</p>

<hr />

<p><em>Built with Java 21, Spring Boot, and a lot of Redis Lua scripts. MIT licensed. Uses Testcontainers for testing because mocking distributed systems is a lie we tell ourselves.</em></p>]]></content><author><name>Burhanettin Nacar</name></author><category term="Distributed Systems" /><category term="Redis" /><category term="Rate Limiting" /><category term="Microservices" /><category term="Java" /><category term="Algorithms" /><category term="Architecture" /><summary type="html"><![CDATA[Building a production-grade distributed rate limiter taught me that algorithm choice matters more than I expected. Here's what I learned implementing Token Bucket, Sliding Window, Fixed Window, Leaky Bucket, and a Composite approach.]]></summary></entry><entry><title type="html">Building a Production-Ready Distributed Rate Limiter with Spring Boot and Redis</title><link href="https://bnacar.dev/2025/09/18/distributed-rate-limiter-spring-boot-redis.html" rel="alternate" type="text/html" title="Building a Production-Ready Distributed Rate Limiter with Spring Boot and Redis" /><published>2025-09-18T00:00:00+00:00</published><updated>2025-09-18T00:00:00+00:00</updated><id>https://bnacar.dev/2025/09/18/distributed-rate-limiter-spring-boot-redis</id><content type="html" xml:base="https://bnacar.dev/2025/09/18/distributed-rate-limiter-spring-boot-redis.html"><![CDATA[<p>Rate limiting is a critical component of any production API system. It protects your services from abuse, ensures fair resource distribution, and maintains system stability under heavy load. In this comprehensive guide, we’ll explore a sophisticated distributed rate limiter implementation built with Spring Boot and Redis that you can deploy in production today.</p>

<h2 id="system-architecture">System Architecture</h2>

<p>The distributed rate limiter follows a straightforward architecture:</p>
<ul>
  <li><strong>Client Applications</strong> → <strong>Load Balancer</strong> → <strong>Spring Boot Instances</strong> → <strong>Redis Cluster</strong></li>
  <li><strong>Monitoring Stack</strong> (Prometheus/Grafana) observes all components</li>
  <li><strong>Kubernetes/Docker</strong> orchestrates deployment and scaling</li>
</ul>

<h2 id="why-distributed-rate-limiting">Why Distributed Rate Limiting?</h2>

<p>Traditional in-memory rate limiters work well for single-instance applications, but fall short in modern distributed systems. When you scale horizontally with multiple service instances, each instance maintains its own rate limit counters, effectively multiplying your allowed request rate by the number of instances.</p>

<p>Our distributed rate limiter solves this by:</p>
<ul>
  <li><strong>Centralized State</strong>: Using Redis as a shared state store across all instances</li>
  <li><strong>Atomic Operations</strong>: Leveraging Lua scripts for thread-safe token consumption</li>
  <li><strong>High Performance</strong>: Sub-millisecond response times with Redis</li>
  <li><strong>Flexible Configuration</strong>: Support for multiple rate limiting strategies</li>
</ul>

<h2 id="core-features">Core Features</h2>

<h3 id="-token-bucket-algorithm">🚀 Token Bucket Algorithm</h3>
<p>The system implements the token bucket algorithm, which provides:</p>
<ul>
  <li>Burst capacity for handling traffic spikes</li>
  <li>Smooth rate limiting over time</li>
  <li>Configurable refill rates</li>
</ul>

<h3 id="-flexible-configuration">🔧 Flexible Configuration</h3>
<ul>
  <li><strong>Per-key limits</strong>: Different limits for different API keys or user IDs</li>
  <li><strong>Pattern-based rules</strong>: Apply limits based on URL patterns or user groups</li>
  <li><strong>Dynamic updates</strong>: Change limits without service restart</li>
  <li><strong>Default fallbacks</strong>: Global defaults with per-resource overrides</li>
</ul>

<h3 id="-comprehensive-monitoring">📊 Comprehensive Monitoring</h3>
<ul>
  <li>Built-in metrics collection</li>
  <li>Health checks and observability</li>
  <li>Performance benchmarking tools</li>
  <li>Grafana dashboard integration</li>
</ul>

<h2 id="quick-start-guide">Quick Start Guide</h2>

<h3 id="prerequisites">Prerequisites</h3>
<ul>
  <li>Java 21+</li>
  <li>Docker and Docker Compose</li>
  <li>Maven 3.8+</li>
</ul>

<h3 id="installation">Installation</h3>

<ol>
  <li><strong>Clone the repository:</strong>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/uppnrise/distributed-rate-limiter.git
<span class="nb">cd </span>distributed-rate-limiter
</code></pre></div>    </div>
  </li>
  <li><strong>Start the services:</strong>
```bash
    <h1 id="start-redis-and-the-application">Start Redis and the application</h1>
    <p>docker-compose up -d</p>
  </li>
</ol>

<h1 id="or-run-locally-with-maven">Or run locally with Maven</h1>
<p>./mvnw spring-boot:run</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
3. **Verify the installation:**
```bash
curl http://localhost:8080/actuator/health
</code></pre></div></div>

<p>You should see a response indicating the service is healthy:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"UP"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"components"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"redis"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"UP"</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="nl">"rateLimiter"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"UP"</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="usage-examples">Usage Examples</h2>

<h3 id="basic-rate-limiting">Basic Rate Limiting</h3>

<p>The simplest use case is checking if a request should be allowed:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://localhost:8080/api/ratelimit/check <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{
    "key": "user:123",
    "tokens": 1
  }'</span>
</code></pre></div></div>

<p><strong>Response:</strong></p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"allowed"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"tokensRemaining"</span><span class="p">:</span><span class="w"> </span><span class="mi">9</span><span class="p">,</span><span class="w">
  </span><span class="nl">"resetTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2025-09-17T10:30:00Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"limit"</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The request flow works as follows:</p>
<ol>
  <li>Client sends request with key and token count</li>
  <li>System checks Redis for existing bucket</li>
  <li>Lua script atomically updates token count</li>
  <li>Response includes remaining tokens and reset time</li>
</ol>

<h3 id="api-key-based-limiting">API Key-Based Limiting</h3>

<p>For API services, you can implement per-key rate limiting:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@RestController</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ApiController</span> <span class="o">{</span>
    
    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">RateLimitService</span> <span class="n">rateLimitService</span><span class="o">;</span>
    
    <span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/api/data"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">ResponseEntity</span><span class="o">&lt;?&gt;</span> <span class="n">getData</span><span class="o">(</span><span class="nd">@RequestHeader</span><span class="o">(</span><span class="s">"X-API-Key"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">apiKey</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">RateLimitRequest</span> <span class="n">request</span> <span class="o">=</span> <span class="nc">RateLimitRequest</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
            <span class="o">.</span><span class="na">key</span><span class="o">(</span><span class="s">"api:"</span> <span class="o">+</span> <span class="n">apiKey</span><span class="o">)</span>
            <span class="o">.</span><span class="na">tokens</span><span class="o">(</span><span class="mi">1</span><span class="o">)</span>
            <span class="o">.</span><span class="na">build</span><span class="o">();</span>
            
        <span class="nc">RateLimitResponse</span> <span class="n">response</span> <span class="o">=</span> <span class="n">rateLimitService</span><span class="o">.</span><span class="na">checkRateLimit</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
        
        <span class="k">if</span> <span class="o">(!</span><span class="n">response</span><span class="o">.</span><span class="na">isAllowed</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">return</span> <span class="nc">ResponseEntity</span><span class="o">.</span><span class="na">status</span><span class="o">(</span><span class="mi">429</span><span class="o">)</span>
                <span class="o">.</span><span class="na">header</span><span class="o">(</span><span class="s">"X-Rate-Limit-Remaining"</span><span class="o">,</span> <span class="s">"0"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">header</span><span class="o">(</span><span class="s">"X-Rate-Limit-Reset"</span><span class="o">,</span> <span class="n">response</span><span class="o">.</span><span class="na">getResetTime</span><span class="o">().</span><span class="na">toString</span><span class="o">())</span>
                <span class="o">.</span><span class="na">body</span><span class="o">(</span><span class="s">"Rate limit exceeded"</span><span class="o">);</span>
        <span class="o">}</span>
        
        <span class="k">return</span> <span class="nc">ResponseEntity</span><span class="o">.</span><span class="na">ok</span><span class="o">()</span>
            <span class="o">.</span><span class="na">header</span><span class="o">(</span><span class="s">"X-Rate-Limit-Remaining"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">valueOf</span><span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">getTokensRemaining</span><span class="o">()))</span>
            <span class="o">.</span><span class="na">body</span><span class="o">(</span><span class="n">fetchData</span><span class="o">());</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="user-based-limiting">User-Based Limiting</h3>

<p>Implement per-user rate limiting for web applications:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserRateLimitInterceptor</span> <span class="kd">implements</span> <span class="nc">HandlerInterceptor</span> <span class="o">{</span>
    
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">preHandle</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span> 
                           <span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span> 
                           <span class="nc">Object</span> <span class="n">handler</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">userId</span> <span class="o">=</span> <span class="n">getCurrentUserId</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
        
        <span class="nc">RateLimitRequest</span> <span class="n">rateLimitRequest</span> <span class="o">=</span> <span class="nc">RateLimitRequest</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
            <span class="o">.</span><span class="na">key</span><span class="o">(</span><span class="s">"user:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">)</span>
            <span class="o">.</span><span class="na">tokens</span><span class="o">(</span><span class="mi">1</span><span class="o">)</span>
            <span class="o">.</span><span class="na">build</span><span class="o">();</span>
            
        <span class="nc">RateLimitResponse</span> <span class="n">rateLimitResponse</span> <span class="o">=</span> <span class="n">rateLimitService</span><span class="o">.</span><span class="na">checkRateLimit</span><span class="o">(</span><span class="n">rateLimitRequest</span><span class="o">);</span>
        
        <span class="k">if</span> <span class="o">(!</span><span class="n">rateLimitResponse</span><span class="o">.</span><span class="na">isAllowed</span><span class="o">())</span> <span class="o">{</span>
            <span class="n">response</span><span class="o">.</span><span class="na">setStatus</span><span class="o">(</span><span class="mi">429</span><span class="o">);</span>
            <span class="n">response</span><span class="o">.</span><span class="na">setHeader</span><span class="o">(</span><span class="s">"Retry-After"</span><span class="o">,</span> <span class="s">"60"</span><span class="o">);</span>
            <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
        <span class="o">}</span>
        
        <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="configuration-management">Configuration Management</h2>

<h3 id="setting-default-limits">Setting Default Limits</h3>

<p>Configure global default limits:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://localhost:8080/api/ratelimit/config/default <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{
    "bucketSize": 100,
    "refillRate": 10,
    "timeWindowSeconds": 60
  }'</span>
</code></pre></div></div>

<h3 id="per-key-configuration">Per-Key Configuration</h3>

<p>Set specific limits for individual keys:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://localhost:8080/api/ratelimit/config/keys/premium-user:456 <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{
    "bucketSize": 1000,
    "refillRate": 100,
    "timeWindowSeconds": 60
  }'</span>
</code></pre></div></div>

<h3 id="pattern-based-rules">Pattern-Based Rules</h3>

<p>Apply limits based on patterns:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://localhost:8080/api/ratelimit/config/patterns/api:premium:<span class="k">*</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{
    "bucketSize": 500,
    "refillRate": 50,
    "timeWindowSeconds": 60
  }'</span>
</code></pre></div></div>

<p>This configuration system provides flexible rate limiting:</p>
<ul>
  <li><strong>Global defaults</strong> apply to all keys unless overridden</li>
  <li><strong>Specific keys</strong> can have custom limits (e.g., premium users)</li>
  <li><strong>Pattern matching</strong> allows bulk configuration (e.g., all API keys starting with “premium:”)</li>
  <li><strong>Runtime updates</strong> change limits without service restart</li>
</ul>

<h3 id="configuration-hierarchy">Configuration Hierarchy</h3>

<p>The system evaluates rate limits in this order:</p>
<ol>
  <li><strong>Specific key match</strong> (e.g., “user:123”)</li>
  <li><strong>Pattern match</strong> (e.g., “api:premium:*”)</li>
  <li><strong>Global default</strong> (fallback for all unmatched keys)</li>
</ol>

<p>This hierarchy allows for sophisticated rate limiting strategies while maintaining simple configuration.</p>

<h2 id="performance-characteristics">Performance Characteristics</h2>

<h3 id="benchmarks">Benchmarks</h3>

<p>Our performance testing shows impressive results:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Run the built-in benchmark</span>
curl <span class="nt">-X</span> POST http://localhost:8080/api/benchmark/run <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{
    "duration": 30,
    "concurrency": 100,
    "requestsPerSecond": 1000
  }'</span>
</code></pre></div></div>

<p><strong>Typical Results:</strong></p>
<ul>
  <li><strong>Latency</strong>: P95 &lt; 2ms, P99 &lt; 5ms</li>
  <li><strong>Throughput</strong>: 50,000+ requests/second</li>
  <li><strong>Memory Usage</strong>: ~100MB for 1M active buckets</li>
  <li><strong>CPU Usage</strong>: &lt;5% under normal load</li>
</ul>

<p>These numbers demonstrate the system’s efficiency:</p>
<ul>
  <li><strong>Sub-millisecond response times</strong> ensure minimal impact on your API</li>
  <li><strong>High throughput capacity</strong> supports enterprise-scale workloads</li>
  <li><strong>Efficient memory usage</strong> through Redis’s optimized data structures</li>
  <li><strong>Low CPU overhead</strong> thanks to Lua script optimization</li>
</ul>

<h3 id="scaling-considerations">Scaling Considerations</h3>

<p>The system scales horizontally with these characteristics:</p>
<ul>
  <li><strong>Redis Cluster</strong>: Supports Redis clustering for massive scale</li>
  <li><strong>Stateless Design</strong>: Application instances can be added/removed freely</li>
  <li><strong>Efficient Memory</strong>: O(1) memory per active rate limit bucket</li>
  <li><strong>Network Optimized</strong>: Lua scripts minimize Redis round trips</li>
</ul>

<h2 id="advanced-use-cases">Advanced Use Cases</h2>

<h3 id="burst-handling">Burst Handling</h3>

<p>The token bucket algorithm naturally handles traffic bursts:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Allow burst of 10 requests, then 1 per second</span>
curl <span class="nt">-X</span> POST http://localhost:8080/api/ratelimit/config/keys/burst-api <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{
    "bucketSize": 10,
    "refillRate": 1,
    "timeWindowSeconds": 1
  }'</span>
</code></pre></div></div>

<h3 id="hierarchical-limits">Hierarchical Limits</h3>

<p>Implement multiple limit tiers:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">HierarchicalRateLimiter</span> <span class="o">{</span>
    
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">checkLimits</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">,</span> <span class="nc">String</span> <span class="n">apiKey</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// Check user limit</span>
        <span class="k">if</span> <span class="o">(!</span><span class="n">checkUserLimit</span><span class="o">(</span><span class="n">userId</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
        <span class="o">}</span>
        
        <span class="c1">// Check API key limit</span>
        <span class="k">if</span> <span class="o">(!</span><span class="n">checkApiKeyLimit</span><span class="o">(</span><span class="n">apiKey</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
        <span class="o">}</span>
        
        <span class="c1">// Check global limit</span>
        <span class="k">return</span> <span class="nf">checkGlobalLimit</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="dynamic-pricing">Dynamic Pricing</h3>

<p>Implement usage-based pricing with rate limits:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UsageTrackingService</span> <span class="o">{</span>
    
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">trackUsage</span><span class="o">(</span><span class="nc">String</span> <span class="n">customerId</span><span class="o">,</span> <span class="kt">int</span> <span class="n">tokens</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// Record usage for billing</span>
        <span class="n">usageRepository</span><span class="o">.</span><span class="na">recordUsage</span><span class="o">(</span><span class="n">customerId</span><span class="o">,</span> <span class="n">tokens</span><span class="o">);</span>
        
        <span class="c1">// Apply dynamic rate limit based on plan</span>
        <span class="nc">CustomerPlan</span> <span class="n">plan</span> <span class="o">=</span> <span class="n">getCustomerPlan</span><span class="o">(</span><span class="n">customerId</span><span class="o">);</span>
        <span class="n">applyPlanLimits</span><span class="o">(</span><span class="n">customerId</span><span class="o">,</span> <span class="n">plan</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="monitoring-and-observability">Monitoring and Observability</h2>

<h3 id="built-in-metrics">Built-in Metrics</h3>

<p>The system exposes comprehensive metrics:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Get system metrics</span>
curl http://localhost:8080/metrics
</code></pre></div></div>

<p><strong>Key Metrics:</strong></p>
<ul>
  <li>Request rates and latencies</li>
  <li>Rate limit hit ratios</li>
  <li>Redis connection health</li>
  <li>Memory and CPU usage</li>
  <li>
    <p>Error rates and types</p>
  </li>
  <li>Redis connectivity</li>
  <li>Error rates and types</li>
</ul>

<h3 id="key-metrics-to-monitor">Key Metrics to Monitor</h3>

<p>Essential metrics for production deployment:</p>

<p><strong>Rate Limiting Metrics:</strong></p>
<ul>
  <li>Requests allowed vs. denied ratio</li>
  <li>Average tokens consumed per request</li>
  <li>Bucket utilization rates</li>
  <li>Configuration changes frequency</li>
</ul>

<p><strong>System Performance:</strong></p>
<ul>
  <li>Response time percentiles (P50, P95, P99)</li>
  <li>Redis connection pool status</li>
  <li>Memory usage trends</li>
  <li>Error rates by type</li>
</ul>

<p><strong>Business Metrics:</strong></p>
<ul>
  <li>API usage by customer tier</li>
  <li>Rate limit violations by endpoint</li>
  <li>Cost optimization opportunities</li>
</ul>

<h3 id="grafana-integration">Grafana Integration</h3>

<h3 id="grafana-integration-1">Grafana Integration</h3>

<p>The project includes pre-built Grafana dashboards:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Start monitoring stack</span>
docker-compose <span class="nt">-f</span> docker-compose.monitoring.yml up <span class="nt">-d</span>
</code></pre></div></div>

<p>Access Grafana at <code class="language-plaintext highlighter-rouge">http://localhost:3000</code> with the pre-configured dashboards.</p>

<h3 id="health-checks">Health Checks</h3>

<p>Multiple health check endpoints ensure system reliability:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Application health</span>
curl http://localhost:8080/actuator/health

<span class="c"># Rate limiter specific health</span>
curl http://localhost:8080/api/ratelimit/health

<span class="c"># Redis connectivity</span>
curl http://localhost:8080/actuator/health/redis
</code></pre></div></div>

<h2 id="security-considerations">Security Considerations</h2>

<h3 id="api-key-validation">API Key Validation</h3>

<p>Secure your rate limiter with proper authentication:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ApiKeyValidator</span> <span class="o">{</span>
    
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">validateApiKey</span><span class="o">(</span><span class="nc">String</span> <span class="n">apiKey</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// Implement your API key validation logic</span>
        <span class="k">return</span> <span class="n">apiKeyRepository</span><span class="o">.</span><span class="na">isValid</span><span class="o">(</span><span class="n">apiKey</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="request-signing">Request Signing</h3>

<p>Implement request signing to prevent abuse:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">RequestSigner</span> <span class="o">{</span>
    
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">verifySignature</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">signature</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="s">"X-Signature"</span><span class="o">);</span>
        <span class="nc">String</span> <span class="n">payload</span> <span class="o">=</span> <span class="n">getRequestPayload</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
        <span class="k">return</span> <span class="nf">hmacSha256</span><span class="o">(</span><span class="n">payload</span><span class="o">,</span> <span class="n">secretKey</span><span class="o">).</span><span class="na">equals</span><span class="o">(</span><span class="n">signature</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="rate-limit-headers">Rate Limit Headers</h3>

<p>Follow standard HTTP headers for rate limiting:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">addRateLimitHeaders</span><span class="o">(</span><span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span> 
                               <span class="nc">RateLimitResponse</span> <span class="n">rateLimitResponse</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">response</span><span class="o">.</span><span class="na">setHeader</span><span class="o">(</span><span class="s">"X-Rate-Limit-Limit"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">valueOf</span><span class="o">(</span><span class="n">rateLimitResponse</span><span class="o">.</span><span class="na">getLimit</span><span class="o">()));</span>
    <span class="n">response</span><span class="o">.</span><span class="na">setHeader</span><span class="o">(</span><span class="s">"X-Rate-Limit-Remaining"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">valueOf</span><span class="o">(</span><span class="n">rateLimitResponse</span><span class="o">.</span><span class="na">getTokensRemaining</span><span class="o">()));</span>
    <span class="n">response</span><span class="o">.</span><span class="na">setHeader</span><span class="o">(</span><span class="s">"X-Rate-Limit-Reset"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">valueOf</span><span class="o">(</span><span class="n">rateLimitResponse</span><span class="o">.</span><span class="na">getResetTime</span><span class="o">().</span><span class="na">getEpochSecond</span><span class="o">()));</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="production-deployment">Production Deployment</h2>

<h3 id="docker-deployment">Docker Deployment</h3>

<p>Use the provided Docker configuration for production:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.8'</span>
<span class="na">services</span><span class="pi">:</span>
  <span class="na">redis</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">redis:7-alpine</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">redis-server --appendonly yes</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">redis-data:/data</span>
    <span class="na">deploy</span><span class="pi">:</span>
      <span class="na">replicas</span><span class="pi">:</span> <span class="m">3</span>
      
  <span class="na">rate-limiter</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">distributed-rate-limiter:latest</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">8080:8080"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">SPRING_PROFILES_ACTIVE=production</span>
      <span class="pi">-</span> <span class="s">SPRING_DATA_REDIS_CLUSTER_NODES=redis:6379</span>
    <span class="na">deploy</span><span class="pi">:</span>
      <span class="na">replicas</span><span class="pi">:</span> <span class="m">3</span>
</code></pre></div></div>

<h3 id="kubernetes-deployment">Kubernetes Deployment</h3>

<p>Deploy to Kubernetes with the provided manifests:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> k8s/
</code></pre></div></div>

<p>The Kubernetes configuration includes:</p>
<ul>
  <li>Horizontal Pod Autoscaler</li>
  <li>Service mesh integration</li>
  <li>Persistent volumes for Redis</li>
  <li>Network policies for security</li>
</ul>

<h3 id="deployment-architecture">Deployment Architecture</h3>

<p>A typical production deployment includes:</p>

<p><strong>Application Tier:</strong></p>
<ul>
  <li>3+ Spring Boot instances for high availability</li>
  <li>Load balancer with health check integration</li>
  <li>Auto-scaling based on CPU/memory metrics</li>
</ul>

<p><strong>Data Tier:</strong></p>
<ul>
  <li>Redis cluster with 3+ master nodes</li>
  <li>Automatic failover and data replication</li>
  <li>Persistent storage for configuration data</li>
</ul>

<p><strong>Monitoring Tier:</strong></p>
<ul>
  <li>Prometheus for metrics collection</li>
  <li>Grafana for visualization and alerting</li>
  <li>Log aggregation with ELK stack</li>
</ul>

<h3 id="environment-configuration">Environment Configuration</h3>

<p>Configure for different environments:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Production settings
</span><span class="py">spring.profiles.active</span><span class="p">=</span><span class="s">production</span>
<span class="py">spring.data.redis.cluster.nodes</span><span class="p">=</span><span class="s">${REDIS_CLUSTER_NODES}</span>
<span class="py">management.endpoint.health.show-details</span><span class="p">=</span><span class="s">never</span>
<span class="py">logging.level.dev.bnacar.distributedratelimiter</span><span class="p">=</span><span class="s">WARN</span>

<span class="c"># Development settings
</span><span class="py">spring.profiles.active</span><span class="p">=</span><span class="s">development</span>
<span class="py">spring.data.redis.host</span><span class="p">=</span><span class="s">localhost</span>
<span class="py">management.endpoint.health.show-details</span><span class="p">=</span><span class="s">always</span>
<span class="py">logging.level.dev.bnacar.distributedratelimiter</span><span class="p">=</span><span class="s">DEBUG</span>
</code></pre></div></div>

<h2 id="best-practices">Best Practices</h2>

<h3 id="1-choose-appropriate-bucket-sizes">1. Choose Appropriate Bucket Sizes</h3>
<ul>
  <li><strong>Small buckets</strong> (1-10): For strict rate limiting</li>
  <li><strong>Medium buckets</strong> (50-100): For typical API usage</li>
  <li><strong>Large buckets</strong> (500+): For batch operations</li>
</ul>

<h3 id="2-monitor-key-metrics">2. Monitor Key Metrics</h3>
<ul>
  <li>Rate limit hit ratio (should be &lt; 5% under normal conditions)</li>
  <li>Average response time (target &lt; 1ms)</li>
  <li>Redis memory usage and connection health</li>
</ul>

<h3 id="3-implement-graceful-degradation">3. Implement Graceful Degradation</h3>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">GracefulRateLimiter</span> <span class="o">{</span>
    
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">checkRateLimit</span><span class="o">(</span><span class="nc">String</span> <span class="n">key</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">rateLimitService</span><span class="o">.</span><span class="na">isAllowed</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="c1">// Log error and allow request in case of rate limiter failure</span>
            <span class="n">log</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"Rate limiter failed, allowing request"</span><span class="o">,</span> <span class="n">e</span><span class="o">);</span>
            <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="4-use-appropriate-ttls">4. Use Appropriate TTLs</h3>
<p>Set TTLs on Redis keys to prevent memory leaks:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Lua script with TTL</span>
<span class="n">redis</span><span class="p">.</span><span class="n">call</span><span class="p">(</span><span class="s1">'EXPIRE'</span><span class="p">,</span> <span class="n">bucket_key</span><span class="p">,</span> <span class="mi">3600</span><span class="p">)</span> <span class="c1">-- 1 hour TTL</span>
</code></pre></div></div>

<h2 id="real-world-use-cases">Real-World Use Cases</h2>

<h3 id="1-e-commerce-platform">1. E-commerce Platform</h3>
<ul>
  <li><strong>Product searches</strong>: 100 requests/minute per user</li>
  <li><strong>API integrations</strong>: 1000 requests/hour per partner</li>
  <li><strong>Admin operations</strong>: 10 requests/minute per admin</li>
</ul>

<h3 id="2-saas-application">2. SaaS Application</h3>
<ul>
  <li><strong>Free tier</strong>: 100 API calls/day</li>
  <li><strong>Pro tier</strong>: 10,000 API calls/day</li>
  <li><strong>Enterprise</strong>: Custom limits based on contract</li>
</ul>

<h3 id="3-iot-data-collection">3. IoT Data Collection</h3>
<ul>
  <li><strong>Device telemetry</strong>: 1 request/second per device</li>
  <li><strong>Firmware updates</strong>: 1 request/hour per device</li>
  <li><strong>Configuration sync</strong>: 10 requests/day per device</li>
</ul>

<h2 id="troubleshooting">Troubleshooting</h2>

<h3 id="common-issues">Common Issues</h3>

<p><strong>High Latency</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check Redis connection</span>
redis-cli ping

<span class="c"># Monitor connection pool</span>
curl http://localhost:8080/actuator/metrics/hikaricp.connections.active
</code></pre></div></div>

<p><strong>Memory Issues</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check Redis memory usage</span>
redis-cli info memory

<span class="c"># Monitor application memory</span>
curl http://localhost:8080/actuator/metrics/jvm.memory.used
</code></pre></div></div>

<p><strong>Configuration Problems</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Validate configuration</span>
curl http://localhost:8080/api/ratelimit/config

<span class="c"># Check logs</span>
docker logs distributed-rate-limiter-app
</code></pre></div></div>

<h2 id="contributing">Contributing</h2>

<p>We welcome contributions! The project follows standard open-source practices:</p>

<ol>
  <li>Fork the repository</li>
  <li>Create a feature branch</li>
  <li>Add tests for new functionality</li>
  <li>Submit a pull request</li>
</ol>

<h3 id="development-setup">Development Setup</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Clone and setup</span>
git clone https://github.com/uppnrise/distributed-rate-limiter.git
<span class="nb">cd </span>distributed-rate-limiter

<span class="c"># Run tests</span>
./mvnw <span class="nb">test</span>

<span class="c"># Run with development profile</span>
./mvnw spring-boot:run <span class="nt">-Dspring-boot</span>.run.profiles<span class="o">=</span>development
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>This distributed rate limiter provides a robust, scalable solution for protecting your APIs and services. With its flexible configuration, comprehensive monitoring, and production-ready design, it’s an excellent choice for modern distributed systems.</p>

<p>Key benefits:</p>
<ul>
  <li>✅ <strong>Production Ready</strong>: Battle-tested algorithms and patterns</li>
  <li>✅ <strong>Highly Scalable</strong>: Handles millions of requests per second</li>
  <li>✅ <strong>Easy Integration</strong>: Simple REST API and Java client</li>
  <li>✅ <strong>Comprehensive Monitoring</strong>: Built-in observability and metrics</li>
  <li>✅ <strong>Flexible Configuration</strong>: Supports complex rate limiting scenarios</li>
</ul>

<h2 id="resources">Resources</h2>

<ul>
  <li><strong>GitHub Repository</strong>: <a href="https://github.com/uppnrise/distributed-rate-limiter">https://github.com/uppnrise/distributed-rate-limiter</a></li>
</ul>]]></content><author><name>Burhanettin Nacar</name></author><category term="Spring Boot" /><category term="Redis" /><category term="Rate Limiting" /><category term="Microservices" /><category term="Java" /><category term="Distributed Systems" /><category term="DevOps" /><category term="Performance" /><summary type="html"><![CDATA[Learn how to build and deploy a sophisticated distributed rate limiter using Spring Boot and Redis. Complete with benchmarks, monitoring, and production deployment strategies.]]></summary></entry></feed>