<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Decoded]]></title><description><![CDATA[Decoded is where I break down backend and distributed systems into explanations that make sense on the first read. From databases and system design to architect]]></description><link>https://curiousnilay.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a6b5afcc83573d66f5ee7ad/3af55c10-9ec3-431b-aadc-9681e9c085cf.jpg</url><title>Decoded</title><link>https://curiousnilay.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 18 Sep 2026 21:14:46 GMT</lastBuildDate><atom:link href="https://curiousnilay.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[What LLM Parameters Actually Do (I Didn’t Even Know Half of These Existed)]]></title><description><![CDATA[When I built my AI financial assistant, I was focused on getting the pipeline working: user query → retrieval → LLM → response. The model was responding correctly, but I realized something embarrassin]]></description><link>https://curiousnilay.hashnode.dev/what-llm-parameters-actually-do-i-didn-t-even-know-half-of-these-existed</link><guid isPermaLink="true">https://curiousnilay.hashnode.dev/what-llm-parameters-actually-do-i-didn-t-even-know-half-of-these-existed</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[openai]]></category><category><![CDATA[backend]]></category><category><![CDATA[OpenAI API]]></category><category><![CDATA[AI Engineering]]></category><dc:creator><![CDATA[Nilay Shahane]]></dc:creator><pubDate>Sat, 15 Aug 2026 19:02:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a6b5afcc83573d66f5ee7ad/eff8cb2f-3440-43be-88d9-3ed1e75d9918.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I built my AI financial assistant, I was focused on getting the pipeline working: user query → retrieval → LLM → response. The model was responding correctly, but I realized something embarrassing.</p>
<p>I had written this:</p>
<pre><code class="language-python">llm = ChatOpenAI(
    model="gpt-4.1-mini",
    temperature=0,
    top_p=1.0,
    max_tokens=512,
    timeout=30,
    max_retries=2
)
</code></pre>
<p>…and I only understood <strong>few</strong> of those parameters (and didn’t even know the others existed).</p>
<p>The rest values were generated by AI.</p>
<p>So I decided to understand every single decision I (my AI) had made. This post is that explanation—the version I wish I had read before building anything with LLMs.</p>
<hr />
<h2>The Configuration</h2>
<pre><code class="language-python">llm = ChatOpenAI(
    model="gpt-4.1-mini",
    temperature=0,
    top_p=1.0,
    max_tokens=512,
    timeout=30,
    max_retries=2
)
</code></pre>
<p>Every one of these changes how your application behaves in production.</p>
<hr />
<h2>Temperature: Creativity vs Consistency</h2>
<p>This was the first parameter that actually clicked for me.</p>
<p><code>temperature</code> controls <strong>how random the model’s output can be</strong>.</p>
<p>Imagine asking:</p>
<blockquote>
<p>"What is the capital of France?"</p>
</blockquote>
<p>With <strong>temperature = 0</strong>, the model will almost always answer:</p>
<blockquote>
<p>Paris.</p>
</blockquote>
<p>With <strong>temperature = 1.0</strong>, the model may still answer Paris, but the wording can vary:</p>
<ul>
<li><p>The capital of France is Paris.</p>
</li>
<li><p>Paris is the capital city of France.</p>
</li>
<li><p>France’s capital is Paris.</p>
</li>
</ul>
<p>Now imagine a different prompt:</p>
<blockquote>
<p>"Write a startup idea involving drones."</p>
</blockquote>
<p>Temperature becomes much more important.</p>
<p><strong>Temperature = 0</strong></p>
<blockquote>
<p>A drone-based package delivery optimization platform.</p>
</blockquote>
<p><strong>Temperature = 1.2</strong></p>
<blockquote>
<p>A swarm of autonomous drones that build temporary communication networks during natural disasters.</p>
</blockquote>
<p>Same prompt, different level of exploration.</p>
<p>For my project, I wanted <strong>deterministic financial explanations</strong>, not creative storytelling.</p>
<p>So I chose:</p>
<pre><code class="language-python">temperature = 0
</code></pre>
<p>Because if a user asks the same financial question twice, I want nearly the same answer twice.</p>
<hr />
<h2>Top-p: How Many Candidate Words the Model Considers</h2>
<p>This parameter confused me for a while because it sounds similar to temperature.</p>
<p>It is different.</p>
<p>The model predicts probabilities for the next word.</p>
<p>Example:</p>
<table>
<thead>
<tr>
<th>Word</th>
<th>Probability</th>
</tr>
</thead>
<tbody><tr>
<td>Paris</td>
<td>0.72</td>
</tr>
<tr>
<td>Lyon</td>
<td>0.10</td>
</tr>
<tr>
<td>Marseille</td>
<td>0.08</td>
</tr>
<tr>
<td>Berlin</td>
<td>0.04</td>
</tr>
<tr>
<td>Madrid</td>
<td>0.03</td>
</tr>
<tr>
<td>Others</td>
<td>0.03</td>
</tr>
</tbody></table>
<p>With:</p>
<pre><code class="language-python">top_p = 1.0
</code></pre>
<p>the model can consider <strong>the full probability distribution</strong>.</p>
<p>With:</p>
<pre><code class="language-python">top_p = 0.8
</code></pre>
<p>it only considers the most probable words whose cumulative probability reaches 0.8.</p>
<p>That means low-probability words are discarded before sampling happens.</p>
<p>A useful intuition:</p>
<ul>
<li><p><strong>Temperature changes randomness</strong></p>
</li>
<li><p><strong>Top-p changes the pool of possible choices</strong></p>
</li>
</ul>
<p>In practice, most applications tune <strong>either temperature or top_p</strong>, not both aggressively.</p>
<p>I kept:</p>
<pre><code class="language-python">top_p = 1.0
</code></pre>
<p>because I was already controlling randomness with temperature.</p>
<hr />
<h2>Max Tokens: Controlling Response Length</h2>
<p>This parameter is surprisingly practical.</p>
<pre><code class="language-python">max_tokens = 512
</code></pre>
<p>means the model can generate <strong>at most 512 output tokens</strong>.</p>
<p>Think of tokens as chunks of text.</p>
<p>Roughly:</p>
<ul>
<li>1 token ≈ 0.75 English words (very approximately)</li>
</ul>
<p>So 512 tokens is usually a few hundred words.</p>
<p>Why does this matter?</p>
<p>Without a limit, a model may produce unnecessarily long responses.</p>
<p>For example:</p>
<p>Prompt:</p>
<blockquote>
<p>"Explain compound interest."</p>
</blockquote>
<p>Unlimited response:</p>
<ul>
<li><p>definition</p>
</li>
<li><p>formula</p>
</li>
<li><p>history</p>
</li>
<li><p>examples</p>
</li>
<li><p>investment strategies</p>
</li>
<li><p>taxation discussion</p>
</li>
<li><p>long conclusion</p>
</li>
</ul>
<p>Limited response:</p>
<ul>
<li><p>definition</p>
</li>
<li><p>formula</p>
</li>
<li><p>short example</p>
</li>
</ul>
<p>For an application, shorter answers often mean:</p>
<ul>
<li><p>lower latency,</p>
</li>
<li><p>lower cost,</p>
</li>
<li><p>more predictable behavior.</p>
</li>
</ul>
<p>I did not need essays.</p>
<p>I needed <strong>concise financial explanations</strong>.</p>
<hr />
<h2>Timeout: Don’t Wait Forever</h2>
<p>This is one of those parameters that matters only when things go wrong.</p>
<pre><code class="language-python">timeout = 30
</code></pre>
<p>means:</p>
<blockquote>
<p>If the model does not respond within 30 seconds, stop waiting.</p>
</blockquote>
<p>Imagine:</p>
<ul>
<li><p>network issues,</p>
</li>
<li><p>API congestion,</p>
</li>
<li><p>provider delays,</p>
</li>
<li><p>temporary outages.</p>
</li>
</ul>
<p>Without a timeout, a request might hang indefinitely.</p>
<p>In a web application, that means users keep staring at a loading spinner.</p>
<p>A timeout lets your application fail gracefully and return an error or retry.</p>
<p>It is not about the model.</p>
<p>It is about <strong>protecting the user experience</strong>.</p>
<hr />
<h2>Max Retries: Surviving Temporary Failures</h2>
<p>APIs fail.</p>
<p>Connections drop.</p>
<p>Rate limits happen.</p>
<p>Transient server errors happen.</p>
<p>This parameter tells the client:</p>
<pre><code class="language-python">max_retries = 2
</code></pre>
<p>If the request fails due to a temporary issue, try again up to <strong>two additional times</strong>.</p>
<p>Example:</p>
<p>Attempt 1 → timeout</p>
<p>Attempt 2 → network reset</p>
<p>Attempt 3 → success</p>
<p>Without retries, that request would have failed even though the service became available moments later.</p>
<p>Retries are one of those production engineering habits that appear everywhere:</p>
<ul>
<li><p>databases,</p>
</li>
<li><p>message queues,</p>
</li>
<li><p>HTTP services,</p>
</li>
<li><p>cloud APIs,</p>
</li>
<li><p>LLM APIs.</p>
</li>
</ul>
<p>They improve reliability without changing your business logic.</p>
<hr />
<h2>Why These Choices Fit My Project</h2>
<p>My project was an AI financial assistant.</p>
<p>The goal was not entertainment.</p>
<p>The goal was <strong>accurate, repeatable explanations</strong>.</p>
<p>So my configuration looked like this:</p>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Choice</th>
<th>Reason</th>
</tr>
</thead>
<tbody><tr>
<td>temperature</td>
<td>0</td>
<td>deterministic answers</td>
</tr>
<tr>
<td>top_p</td>
<td>1.0</td>
<td>no additional sampling restriction</td>
</tr>
<tr>
<td>max_tokens</td>
<td>512</td>
<td>concise responses</td>
</tr>
<tr>
<td>timeout</td>
<td>30</td>
<td>prevent hanging requests</td>
</tr>
<tr>
<td>max_retries</td>
<td>2</td>
<td>recover from transient API failures</td>
</tr>
</tbody></table>
<p>What looked like random numbers started looking like <strong>engineering decisions</strong>.</p>
<hr />
<h2>Where I Would Use Different Values</h2>
<h3>Creative writing</h3>
<pre><code class="language-python">temperature = 1.2
top_p = 0.95
max_tokens = 1500
</code></pre>
<p>Useful for stories, poems, marketing copy, and brainstorming.</p>
<h3>Code generation</h3>
<pre><code class="language-python">temperature = 0
top_p = 1.0
max_tokens = 800
</code></pre>
<p>Consistency matters more than novelty.</p>
<h3>Customer support chatbot</h3>
<pre><code class="language-python">temperature = 0.2
top_p = 1.0
timeout = 10
max_retries = 3
</code></pre>
<p>Fast, reliable, and mostly deterministic.</p>
<h3>Research assistant</h3>
<pre><code class="language-python">temperature = 0.4
top_p = 0.9
max_tokens = 2000
</code></pre>
<p>Allows slightly broader reasoning and longer explanations.</p>
<hr />
<h2>The Lesson That Actually Stayed With Me</h2>
<p>Before this project, I used LLM parameters the same way beginners use Redis commands or Docker flags.</p>
<p>Copy.</p>
<p>Paste.</p>
<p>Hope.</p>
<p>After spending a few hours understanding them, I stopped seeing them as magic numbers.</p>
<p>Now when I read:</p>
<pre><code class="language-python">temperature=0
</code></pre>
<p>I immediately think:</p>
<blockquote>
<p>“This system wants deterministic behavior.”</p>
</blockquote>
<p>When I read:</p>
<pre><code class="language-python">max_tokens=128
</code></pre>
<p>I think:</p>
<blockquote>
<p>“Someone optimized for latency and cost.”</p>
</blockquote>
<p>When I read:</p>
<pre><code class="language-python">max_retries=5
</code></pre>
<p>I think:</p>
<blockquote>
<p>“Reliability mattered in this application.”</p>
</blockquote>
<p>That was the real shift.</p>
<p>I didn’t just learn what these parameters do.</p>
<p>I learned to read an LLM configuration and understand <strong>the design priorities behind it</strong>.</p>
<p>And that feels a lot more useful than memorizing another API call.</p>
]]></content:encoded></item><item><title><![CDATA[I Tried Designing a Distributed Rate Limiter. Here’s Every Place It Broke.]]></title><description><![CDATA[Somewhere out there is a client with a bug — a while (true) loop with a missing break condition, hammering /api/order/create as fast as the network allows.
while (true) {
  await fetch('/api/order/cre]]></description><link>https://curiousnilay.hashnode.dev/i-tried-designing-a-distributed-rate-limiter-here-s-every-place-it-broke</link><guid isPermaLink="true">https://curiousnilay.hashnode.dev/i-tried-designing-a-distributed-rate-limiter-here-s-every-place-it-broke</guid><category><![CDATA[System Design]]></category><category><![CDATA[distributedsystems]]></category><category><![CDATA[Redis]]></category><category><![CDATA[Backend Development]]></category><category><![CDATA[#backenddevelopment]]></category><category><![CDATA[#softwareengineering]]></category><category><![CDATA[distributed systems design]]></category><category><![CDATA[rate-limiting]]></category><category><![CDATA[ratelimit]]></category><category><![CDATA[#ratelimiting ]]></category><dc:creator><![CDATA[Nilay Shahane]]></dc:creator><pubDate>Sat, 15 Aug 2026 17:37:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a6b5afcc83573d66f5ee7ad/53313a33-3049-4ba4-aca0-4cc0826d361d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Somewhere out there is a client with a bug — a <code>while (true)</code> loop with a missing break condition, hammering <code>/api/order/create</code> as fast as the network allows.</p>
<pre><code class="language-plaintext">while (true) {
  await fetch('/api/order/create');
}
</code></pre>
<p>Your server has no idea this is a bug. It just sees traffic. CPU usage spikes, your database starts struggling, other users experience latency, and your cloud bill gets ugly. Replace the bug with a malicious user trying 10,000 passwords against <code>/login</code>, and the story gets worse.</p>
<p>That is the entire reason <strong>rate limiting</strong> exists.</p>
<p>A rate limiter answers one question before your business logic runs:</p>
<blockquote>
<p><strong>Is this user allowed to make another request right now?</strong></p>
</blockquote>
<p>If yes, continue.</p>
<p>If no, return <strong>HTTP 429 (Too Many Requests)</strong>.</p>
<p>I used to think this was one of those backend topics every framework had already solved. Then I tried designing one from scratch, and it broke in more places than I expected.</p>
<p>This post is that journey — mistakes included.</p>
<hr />
<h2>Attempt 1: Just Use a HashMap</h2>
<p>The rule I picked was simple:</p>
<p><strong>100 requests per minute per user</strong></p>
<p>My first instinct was a hash map.</p>
<pre><code class="language-plaintext">userId → count
</code></pre>
<p>Constant-time lookups. Trivial to increment. On a single server, this genuinely works.</p>
<p>I was fairly pleased with myself for about five minutes.</p>
<hr />
<h2>Where It Broke: There Is No “The Server”</h2>
<p>Real systems rarely have a single server.</p>
<p>A typical deployment looks like this:</p>
<pre><code class="language-plaintext">Client
   |
Load Balancer
   |
+---------+   +---------+   +---------+
| Server A|   | Server B|   | Server C|
+---------+   +---------+   +---------+
</code></pre>
<p>If <code>user123</code> sends 100 requests, the load balancer may distribute them across all three servers.</p>
<ul>
<li><p>Server A sees 30</p>
</li>
<li><p>Server B sees 40</p>
</li>
<li><p>Server C sees 30</p>
</li>
</ul>
<p>None of them knows the <strong>real total</strong>.</p>
<p>This was my first distributed-systems lesson:</p>
<blockquote>
<p><strong>The counter cannot live inside the application process.</strong></p>
</blockquote>
<p>It has to live somewhere <strong>every server can access</strong>.</p>
<hr />
<h2>Why Redis Instead of PostgreSQL?</h2>
<p>My next question was obvious.</p>
<p>Why not store the counter in PostgreSQL?</p>
<p>Technically, you can.</p>
<p>But rate limiting is a <strong>write-heavy, latency-sensitive</strong> workload. Every request needs to update a counter before any business logic executes.</p>
<p>Redis is a much better fit because:</p>
<ul>
<li><p>it stores data in memory,</p>
</li>
<li><p>counter operations are extremely fast,</p>
</li>
<li><p>keys can expire automatically using <strong>TTL</strong>,</p>
</li>
<li><p>and it is commonly used for ephemeral state such as caches, locks, and rate limiters.</p>
</li>
</ul>
<p>So my design became:</p>
<pre><code class="language-plaintext">rl:user:123 → 57
</code></pre>
<p>stored in Redis.</p>
<hr />
<h2>Where It Broke Again: The Race Condition</h2>
<p>At this point I thought I was done.</p>
<p>Then I imagined two servers receiving requests from the same user at the <strong>exact same moment</strong>.</p>
<p>Server A:</p>
<pre><code class="language-plaintext">Read count = 99
</code></pre>
<p>Server B:</p>
<pre><code class="language-plaintext">Read count = 99
</code></pre>
<p>Both increment.</p>
<p>Both allow the request.</p>
<p>The user just made <strong>101 requests</strong> even though the limit is 100.</p>
<p>The problem was not Redis itself.</p>
<p>The problem was my sequence of operations.</p>
<ol>
<li><p>Check if the key exists</p>
</li>
<li><p>Create it if necessary</p>
</li>
<li><p>Read the count</p>
</li>
<li><p>Compare with the limit</p>
</li>
<li><p>Increment</p>
</li>
<li><p>Return allow/reject</p>
</li>
</ol>
<p>Across multiple servers, those operations are <strong>not atomic</strong>.</p>
<hr />
<h2>The Fix: Push the Whole Operation Into Redis</h2>
<p>This is where <strong>Lua scripting</strong> finally made sense to me.</p>
<p>Redis executes commands sequentially, and a Lua script is executed <strong>atomically</strong>. The entire script completes before another command can modify the same data.</p>
<p>Instead of sending multiple Redis commands from Node.js, the script can perform everything in one step:</p>
<ul>
<li><p>increment the counter,</p>
</li>
<li><p>create the key if necessary,</p>
</li>
<li><p>set the expiration time,</p>
</li>
<li><p>compare against the limit,</p>
</li>
<li><p>return allow or reject.</p>
</li>
</ul>
<p>No other request can observe the counter halfway through the operation.</p>
<p>The race condition disappears.</p>
<hr />
<h2>I Started With Redis Hashes</h2>
<p>My initial design used a Redis <strong>Hash</strong>.</p>
<pre><code class="language-plaintext">user123 → count
</code></pre>
<p>Later I realized something interesting.</p>
<p>For a single numeric counter, a Redis <strong>String</strong> is actually simpler.</p>
<pre><code class="language-plaintext">rl:user:123 = 57
</code></pre>
<p>Redis can increment strings directly, so there is no need for a hash unless I want multiple fields such as <code>count</code>, <code>plan</code>, <code>lastSeen</code>, etc.</p>
<p>It was a small lesson, but an important one:</p>
<blockquote>
<p><strong>The simplest data structure that satisfies the requirement is usually the best one.</strong></p>
</blockquote>
<h2>Where It Broke a Third Time: The Fixed Window Problem</h2>
<p>At this point I had a rate limiter that worked correctly across multiple servers.</p>
<p>Then I noticed this.</p>
<p>A user sends:</p>
<ul>
<li><p>100 requests at <strong>12:00:59</strong></p>
</li>
<li><p>another 100 requests at <strong>12:01:00</strong></p>
</li>
</ul>
<p>They successfully make <strong>200 requests within two seconds</strong>.</p>
<p>The counter resets exactly on the minute boundary, so the algorithm has no memory of what happened one second earlier.</p>
<p>This algorithm is called a <strong>Fixed Window Counter</strong>.</p>
<p>It is fast and simple, but it has boundary problems.</p>
<hr />
<h2>Attempt 2: Stop Counting, Start Remembering <em>When</em></h2>
<p>Instead of storing only a counter, I started thinking about storing <strong>the timestamp of every request</strong>.</p>
<p>Suppose requests arrive at:</p>
<ul>
<li><p>t = 0</p>
</li>
<li><p>t = 1</p>
</li>
<li><p>t = 3</p>
</li>
</ul>
<p>If the window size is 5 seconds, then at t = 5 the request from t = 0 has expired.</p>
<p>Only the requests from t = 1 and t = 3 still count.</p>
<p>The rate limiter is now evaluating the <strong>last N seconds</strong>, not the last calendar minute.</p>
<p>A common implementation stores timestamps in a Redis <strong>Sorted Set (ZSET)</strong>.</p>
<p>For every request:</p>
<ol>
<li><p>Remove timestamps older than <code>now - window</code></p>
</li>
<li><p>Count the remaining timestamps</p>
</li>
<li><p>If below the limit, insert the current timestamp</p>
</li>
<li><p>Otherwise reject the request</p>
</li>
</ol>
<p>This is the <strong>Sliding Window</strong> approach.</p>
<p>It almost completely removes the fixed-window boundary problem.</p>
<hr />
<h2>The Tradeoff I Didn’t Expect to Care About</h2>
<table>
<thead>
<tr>
<th>Fixed Window</th>
<th>Sliding Window</th>
</tr>
</thead>
<tbody><tr>
<td>Very fast</td>
<td>More expensive</td>
</tr>
<tr>
<td>Simple</td>
<td>More complex</td>
</tr>
<tr>
<td>Stores one integer</td>
<td>Stores timestamps</td>
</tr>
<tr>
<td>Boundary spikes possible</td>
<td>Smooth limiting</td>
</tr>
</tbody></table>
<p>I initially assumed sliding window was simply “better.”</p>
<p>It isn’t.</p>
<p>It is <strong>more accurate</strong>, but it is also more expensive in memory and CPU.</p>
<p>For many production APIs, fixed windows are still a perfectly reasonable choice because operational simplicity often matters more than eliminating a rare edge case.</p>
<hr />
<h2>Where This Design Still Fails in Production</h2>
<p>I thought the design was finished.</p>
<p>Then I imagined one enterprise customer sending <strong>50,000 requests per second</strong>.</p>
<p>Every request updates the same Redis key:</p>
<pre><code class="language-plaintext">rl:user:enterprise123
</code></pre>
<p>Now every Node.js server is hammering the <strong>same Redis key</strong>.</p>
<p>Redis executes commands sequentially, so all requests for that key are effectively <strong>serialized through the same Redis shard</strong>.</p>
<p>The architecture becomes:</p>
<pre><code class="language-plaintext">Many Node.js servers
          |
          v
    One Redis key
          |
          v
 Serialized execution
</code></pre>
<p>At moderate scale, this is fine.</p>
<p>At very high scale, that key becomes a <strong>hot key bottleneck</strong>.</p>
<p>The bottleneck is no longer my application.</p>
<p>It is Redis itself.</p>
<hr />
<h1>How I Would Improve It Next</h1>
<p>I have not implemented these yet, but these are the directions I would explore next:</p>
<ul>
<li><p><strong>Redis Cluster</strong> to distribute data across multiple shards</p>
</li>
<li><p><strong>Sharded counters</strong> where one logical counter is split across multiple keys</p>
</li>
<li><p><strong>Token Bucket</strong> algorithms for burst-friendly traffic</p>
</li>
<li><p><strong>Leaky Bucket</strong> algorithms for traffic smoothing</p>
</li>
<li><p><strong>Local in-memory caching</strong> for extremely hot keys</p>
</li>
<li><p><strong>Multi-region rate limiting</strong> for globally distributed deployments</p>
</li>
</ul>
<p>I intentionally stopped before these optimizations because I wanted to understand <strong>one production-ready version deeply</strong> before making it internet-scale.</p>
<hr />
<h2>What Actually Stuck With Me</h2>
<p>I started this exercise thinking a rate limiter was just “store a number somewhere.”</p>
<p>It turned into a lesson about:</p>
<ul>
<li><p>shared state across servers,</p>
</li>
<li><p>atomicity,</p>
</li>
<li><p>expiration semantics,</p>
</li>
<li><p>algorithm tradeoffs,</p>
</li>
<li><p>and bottlenecks that only appear once you stop imagining toy traffic.</p>
</li>
</ul>
<p>That buggy <code>while (true)</code> loop from the beginning of this post is still the entire point.</p>
<p>Every layer I added — Redis, Lua, sliding windows — exists because somewhere, someone’s client is going to do exactly that, and your server needs an answer ready before it happens.</p>
<p>I don’t think this is the final version of a rate limiter.</p>
<p>I think it is the first version I’d be comfortable defending in a backend interview.</p>
<p>And that, for me, was the interesting part.</p>
]]></content:encoded></item></channel></rss>