<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Rate-Limiting on vishctl</title><link>https://vishctl.dev/tags/rate-limiting/</link><description>Recent content in Rate-Limiting on vishctl</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 13 Oct 2026 02:42:00 +0800</lastBuildDate><atom:link href="https://vishctl.dev/tags/rate-limiting/index.xml" rel="self" type="application/rss+xml"/><item><title>Rate Limiting LLM APIs: Count Tokens, Not Requests</title><link>https://vishctl.dev/posts/llm-rate-limiting-load-shedding-tokens/</link><pubDate>Tue, 13 Oct 2026 02:42:00 +0800</pubDate><guid>https://vishctl.dev/posts/llm-rate-limiting-load-shedding-tokens/</guid><category>rate-limiting</category><category>multi-tenancy</category><category>inference</category><category>api-gateway</category><category>reliability</category><category>ai-infra</category><description>A diagram-first guide to token budgets, fair admission, and load shedding for shared LLM APIs, with a runnable Python simulation.</description><content:encoded><![CDATA[<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Request limits count calls: a short chat and a long report each count as one
</span></span><span class="line"><span class="cl">Token budgets count volume: large prompts and long answers spend more budget
</span></span></code></pre></div><p>Most API gateways can limit requests per minute. That is a useful first guardrail when requests have similar costs.</p>
<p>LLM requests can vary widely. One call asks for a 20-token answer to a 50-token question. The next sends a 100,000-token document and asks for a 4,000-token report.</p>
<p>A request counter sees &ldquo;one request&rdquo; both times. Token volume gives you a better starting point for a budget, although input and output tokens do not cost the same amount of GPU work.</p>
<figure class="mermaid-figure">
  <div class="mermaid">flowchart TD
    subgraph REQ["Counting requests"]
        direction LR
        A1["Small request<br/>70 tokens"] a1@--> A3["Counter says<br/>1 and 1"]
        A2["Huge request<br/>104,000 tokens"] a2@--> A3
        A3 a3@--> A4["Both allowed<br/>GPU swamped"]
    end

    subgraph TOK["Counting tokens"]
        direction LR
        B1["Small request<br/>70 tokens"] b1@--> B3["Counter says<br/>70 and 104,000"]
        B2["Huge request<br/>104,000 tokens"] b2@--> B3
        B3 b3@--> B4["Budget enforced<br/>GPU protected"]
    end

    REQ ~~~ TOK
    a1@{ animate: true }
    a2@{ animate: true }
    a3@{ animate: true }
    b1@{ animate: true }
    b2@{ animate: true }
    b3@{ animate: true }

    classDef input fill:#13264a,stroke:#38bdf8,color:#e6f6ff,stroke-width:2px;
    classDef warn fill:#302410,stroke:#fbbf24,color:#fff3cf,stroke-width:2px;
    classDef idle fill:#101e27,stroke:#324950,color:#8aa0a8;
    classDef tree fill:#281d48,stroke:#a78bfa,color:#f1edff,stroke-width:2px;
    classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff,stroke-width:2px;
    class A1,B1 input;
    class A2,B2,A4 warn;
    class A3 idle;
    class B3 tree;
    class B4 gpu;</div>
  <figcaption>Top: both requests count as one, so the heavy one slips through. Bottom: counted in tokens, the heavy request is roughly 1,486 times larger.</figcaption>
</figure>

<p>This post covers the two protections every shared model endpoint needs: a <strong>token budget</strong> per tenant, and <strong>load shedding</strong> for the moments when the whole server is full.</p>
<p>It covers what to do once you have hit the maximum number of replicas and traffic keeps coming.</p>
<h2 id="why-request-limits-fail">Why request limits fail</h2>
<p>Put three teams on one model server. Each gets the same limit: 2 requests per second.</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Team</th>
            <th scope="col">Requests per second</th>
            <th scope="col">Tokens per request</th>
            <th scope="col">Tokens per second</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Heavy (document analysis)</td>
            <td>2</td>
            <td>8,000</td>
            <td>16,000</td>
        </tr>
        <tr>
            <td>Light 1 (chat)</td>
            <td>2</td>
            <td>400</td>
            <td>800</td>
        </tr>
        <tr>
            <td>Light 2 (chat)</td>
            <td>2</td>
            <td>400</td>
            <td>800</td>
        </tr>
    </tbody>
  </table>
</div>
<p>Everyone is inside the limit. Nobody did anything wrong. Yet one team submits <strong>90.9% of the tokens</strong>. That can crowd out the chat teams, even though the exact latency impact depends on batching, prompt caching, and the mix of input and output tokens.</p>
<p>Request counts alone miss that difference in workload. Keep a request limit for bursts, and add a token budget for volume.</p>
<h2 id="the-token-bucket">The token bucket</h2>
<p>The fix is an old networking idea with a fitting name.</p>
<p>Imagine a bucket that holds tokens. It refills at a steady rate.</p>
<p>Every request must take out as many tokens as it will use. If the bucket does not hold enough, the request is refused.</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Setting</th>
            <th scope="col">Meaning</th>
            <th scope="col">Example</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Refill rate</td>
            <td>The sustained budget</td>
            <td>1,000 tokens per second</td>
        </tr>
        <tr>
            <td>Capacity</td>
            <td>How big a burst is allowed</td>
            <td>8,000 tokens</td>
        </tr>
    </tbody>
  </table>
</div>
<p>Watch one tenant with that bucket send four requests:</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Step</th>
            <th scope="col">Time</th>
            <th scope="col">Request size</th>
            <th scope="col">Bucket before</th>
            <th scope="col">Decision</th>
            <th scope="col">Bucket after</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>1</td>
            <td>0 s</td>
            <td>6,000</td>
            <td>8,000</td>
            <td>Allow</td>
            <td>2,000</td>
        </tr>
        <tr>
            <td>2</td>
            <td>1 s</td>
            <td>6,000</td>
            <td>3,000</td>
            <td>Reject</td>
            <td>3,000</td>
        </tr>
        <tr>
            <td>3</td>
            <td>2 s</td>
            <td>400</td>
            <td>4,000</td>
            <td>Allow</td>
            <td>3,600</td>
        </tr>
        <tr>
            <td>4</td>
            <td>6 s</td>
            <td>6,000</td>
            <td>7,600</td>
            <td>Allow</td>
            <td>1,600</td>
        </tr>
    </tbody>
  </table>
</div>
<p>Three things happened there, and all three are what you want:</p>
<ul>
<li><strong>A burst was allowed</strong> (step 1). A full bucket absorbs an occasional large request.</li>
<li><strong>A second burst was refused</strong> (step 2). Sustained heavy use is not possible.</li>
<li><strong>Small requests kept flowing</strong> (step 3). Light traffic is barely affected by the limit.</li>
</ul>
<h3 id="how-many-tokens-does-a-request-cost">How many tokens does a request cost?</h3>
<p>You must decide the cost <strong>before</strong> running the request, and half of it is unknown.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Cost = input tokens (known) + output tokens (not known yet)
</span></span></code></pre></div><p>For a gateway you control, one approach is to reserve the worst case and refund the rest:</p>
<ol>
<li><strong>Reserve</strong> <code>input tokens + max_tokens</code> when the request arrives.</li>
<li><strong>Run</strong> the request.</li>
<li><strong>Refund</strong> the unused output tokens when it finishes.</li>
</ol>
<p>Treat <code>max_tokens</code> here as your output cap; the API field name varies. Hosted providers can use different accounting rules.</p>
<p>This is also a good reason to make clients set a sensible <code>max_tokens</code>. A client that always asks for the maximum reserves budget it never uses.</p>
<p>The budget is an approximation of compute cost. Prefill handles input tokens in parallel, while decode generates output tokens step by step. For tighter control, use separate input and output budgets or measured weights, plus an in-flight request limit.</p>
<p>A reservation larger than the bucket capacity will never fit, even after waiting. Reject it with a clear size or budget error so the client can shorten the request. In production, make reservations atomic across gateway replicas and reconcile them when requests finish or are cancelled.</p>
<h2 id="try-it-one-heavy-tenant-two-limits">Try it: one heavy tenant, two limits</h2>
<p>This script runs the three-team example through both kinds of limit. Save it as <code>token_limit.py</code>. It uses only the Python standard library and models admission decisions, not a GPU or a queue.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;token_limit.py: request limits versus token limits for three tenants.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">TokenBucket</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Holds up to `capacity` units and refills at `rate` units per second.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">rate</span><span class="p">,</span> <span class="n">capacity</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">rate</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">capacity</span> <span class="o">=</span> <span class="n">rate</span><span class="p">,</span> <span class="n">capacity</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">level</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">last</span> <span class="o">=</span> <span class="n">capacity</span><span class="p">,</span> <span class="mf">0.0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">allow</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">cost</span><span class="p">,</span> <span class="n">now</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">level</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">capacity</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">level</span> <span class="o">+</span> <span class="p">(</span><span class="n">now</span> <span class="o">-</span> <span class="bp">self</span><span class="o">.</span><span class="n">last</span><span class="p">)</span> <span class="o">*</span> <span class="bp">self</span><span class="o">.</span><span class="n">rate</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">last</span> <span class="o">=</span> <span class="n">now</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">cost</span> <span class="o">&gt;</span> <span class="bp">self</span><span class="o">.</span><span class="n">level</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="kc">False</span>              <span class="c1"># reject now, do not queue</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">level</span> <span class="o">-=</span> <span class="n">cost</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># name: (requests per second, tokens per request)</span>
</span></span><span class="line"><span class="cl"><span class="n">tenants</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;heavy&#34;</span><span class="p">:</span> <span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">8000</span><span class="p">),</span> <span class="s2">&#34;light-1&#34;</span><span class="p">:</span> <span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">400</span><span class="p">),</span> <span class="s2">&#34;light-2&#34;</span><span class="p">:</span> <span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">400</span><span class="p">)}</span>
</span></span><span class="line"><span class="cl"><span class="n">SECONDS</span> <span class="o">=</span> <span class="mi">60</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="n">make_bucket</span><span class="p">,</span> <span class="n">cost_of</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">admitted</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="p">(</span><span class="n">rps</span><span class="p">,</span> <span class="n">tokens</span><span class="p">)</span> <span class="ow">in</span> <span class="n">tenants</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">bucket</span><span class="p">,</span> <span class="n">ok</span> <span class="o">=</span> <span class="n">make_bucket</span><span class="p">(),</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">SECONDS</span> <span class="o">*</span> <span class="n">rps</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">bucket</span><span class="o">.</span><span class="n">allow</span><span class="p">(</span><span class="n">cost_of</span><span class="p">(</span><span class="n">tokens</span><span class="p">),</span> <span class="n">now</span><span class="o">=</span><span class="n">i</span> <span class="o">/</span> <span class="n">rps</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">                <span class="n">ok</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">        <span class="n">admitted</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">ok</span> <span class="o">*</span> <span class="n">tokens</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">admitted</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">by_request</span> <span class="o">=</span> <span class="n">run</span><span class="p">(</span><span class="k">lambda</span><span class="p">:</span> <span class="n">TokenBucket</span><span class="p">(</span><span class="n">rate</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">capacity</span><span class="o">=</span><span class="mi">2</span><span class="p">),</span> <span class="n">cost_of</span><span class="o">=</span><span class="k">lambda</span> <span class="n">tokens</span><span class="p">:</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">by_token</span> <span class="o">=</span> <span class="n">run</span><span class="p">(</span><span class="k">lambda</span><span class="p">:</span> <span class="n">TokenBucket</span><span class="p">(</span><span class="n">rate</span><span class="o">=</span><span class="mi">1000</span><span class="p">,</span> <span class="n">capacity</span><span class="o">=</span><span class="mi">8000</span><span class="p">),</span> <span class="n">cost_of</span><span class="o">=</span><span class="k">lambda</span> <span class="n">tokens</span><span class="p">:</span> <span class="n">tokens</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">title</span><span class="p">,</span> <span class="n">result</span> <span class="ow">in</span> <span class="p">((</span><span class="s2">&#34;Limit: 2 requests per second each&#34;</span><span class="p">,</span> <span class="n">by_request</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                      <span class="p">(</span><span class="s2">&#34;Limit: 1,000 tokens per second each&#34;</span><span class="p">,</span> <span class="n">by_token</span><span class="p">)):</span>
</span></span><span class="line"><span class="cl">    <span class="n">total</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">result</span><span class="o">.</span><span class="n">values</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">title</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">tokens</span> <span class="ow">in</span> <span class="n">result</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;  </span><span class="si">{</span><span class="n">name</span><span class="si">:</span><span class="s2">8</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">tokens</span> <span class="o">/</span> <span class="n">SECONDS</span><span class="si">:</span><span class="s2">8,.0f</span><span class="si">}</span><span class="s2"> tokens/s   </span><span class="si">{</span><span class="mi">100</span> <span class="o">*</span> <span class="n">tokens</span> <span class="o">/</span> <span class="n">total</span><span class="si">:</span><span class="s2">5.1f</span><span class="si">}</span><span class="s2">% of admitted tokens&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;  </span><span class="si">{</span><span class="s1">&#39;total&#39;</span><span class="si">:</span><span class="s2">8</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">total</span> <span class="o">/</span> <span class="n">SECONDS</span><span class="si">:</span><span class="s2">8,.0f</span><span class="si">}</span><span class="s2"> tokens/s</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 token_limit.py
</span></span></code></pre></div><p>The output:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Limit: 2 requests per second each
</span></span><span class="line"><span class="cl">  heavy      16,000 tokens/s    90.9% of admitted tokens
</span></span><span class="line"><span class="cl">  light-1       800 tokens/s     4.5% of admitted tokens
</span></span><span class="line"><span class="cl">  light-2       800 tokens/s     4.5% of admitted tokens
</span></span><span class="line"><span class="cl">  total      17,600 tokens/s
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Limit: 1,000 tokens per second each
</span></span><span class="line"><span class="cl">  heavy       1,067 tokens/s    40.0% of admitted tokens
</span></span><span class="line"><span class="cl">  light-1       800 tokens/s    30.0% of admitted tokens
</span></span><span class="line"><span class="cl">  light-2       800 tokens/s    30.0% of admitted tokens
</span></span><span class="line"><span class="cl">  total       2,667 tokens/s
</span></span></code></pre></div><p>Same class, same three tenants. The only change is the unit being counted.</p>
<p>With the token limit, all chat requests are still admitted: each light tenant keeps its full 800 tokens per second. The heavy tenant is held near its sustained budget. The initial 8,000-token burst explains why its 60-second average is slightly above 1,000 tokens per second.</p>
<p>This demonstrates admission fairness by token volume. It does not predict response latency or GPU utilization.</p>
<p>Notice the same <code>TokenBucket</code> class did both jobs. A request limit is just a token bucket where every request costs 1.</p>
<h2 id="where-the-limit-lives">Where the limit lives</h2>
<p>Rate limiting belongs at the front door, before a request can take a slot on the GPU.</p>
<figure class="mermaid-figure">
  <div class="mermaid">flowchart TD
    C["Client<br/>with API key"] g1@--> G["Gateway<br/>token budget<br/>per tenant"]
    G g2@--> R["Router<br/>picks a replica"]
    R g3@--> V["vLLM<br/>queue and GPU"]
    G g4@--> X["Over budget<br/>429 in<br/>milliseconds"]

    g1@{ animate: true }
    g2@{ animate: true }
    g3@{ animate: true }
    g4@{ animate: true }

    classDef input fill:#13264a,stroke:#38bdf8,color:#e6f6ff,stroke-width:2px;
    classDef tree fill:#281d48,stroke:#a78bfa,color:#f1edff,stroke-width:2px;
    classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff,stroke-width:2px;
    classDef warn fill:#302410,stroke:#fbbf24,color:#fff3cf,stroke-width:2px;
    class C input;
    class G,R tree;
    class V gpu;
    class X warn;</div>
  <figcaption>The budget check happens at the gateway. A request that is over budget is refused in milliseconds and never reaches the model server&#39;s queue.</figcaption>
</figure>

<p>Three practical points:</p>
<ul>
<li><strong>Key the budget on the tenant</strong>, not the IP address. Use the API key, team, or application.</li>
<li><strong>Share the count across gateway replicas.</strong> If each replica keeps its own bucket in memory, three replicas means three times the budget. Use a shared store such as Redis with an atomic refill-and-reserve operation.</li>
<li><strong>Tell clients where they stand.</strong> Hosted APIs return headers such as <code>x-ratelimit-remaining-tokens</code> for this. Doing the same lets well-behaved clients slow down before they hit the wall.</li>
</ul>
<h2 id="load-shedding-when-the-whole-server-is-full">Load shedding: when the whole server is full</h2>
<p>A token budget protects tenants from each other. It does not protect the server from everyone at once.</p>
<p>For a simplified example with equally costly requests, suppose your replicas can finish 10 requests per second, you are already at the maximum replica count, and 15 requests per second arrive. Each tenant is inside its own budget.</p>
<p>Without a plan, the extra requests wait in the queue. The queue never drains, because arrivals keep beating departures:</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Time</th>
            <th scope="col">Arrived</th>
            <th scope="col">Finished</th>
            <th scope="col">Waiting</th>
            <th scope="col">Wait for a new request</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>0 s</td>
            <td>0</td>
            <td>0</td>
            <td>0</td>
            <td>0 s</td>
        </tr>
        <tr>
            <td>20 s</td>
            <td>300</td>
            <td>200</td>
            <td>100</td>
            <td>10 s</td>
        </tr>
        <tr>
            <td>40 s</td>
            <td>600</td>
            <td>400</td>
            <td>200</td>
            <td>20 s</td>
        </tr>
        <tr>
            <td>60 s</td>
            <td>900</td>
            <td>600</td>
            <td>300</td>
            <td>30 s</td>
        </tr>
    </tbody>
  </table>
</div>
<p>After one minute, every new request waits 30 seconds before it even starts. If clients have a 30-second total timeout, queued requests can expire before they receive an answer. Unless cancellation reaches the backend, the GPU may keep working on answers nobody is waiting for.</p>
<p>Throughput can stay high while <strong>goodput collapses</strong>: fewer answers reach clients within their deadline.</p>
<figure class="mermaid-figure">
  <div class="mermaid">flowchart TD
    subgraph NOSHED["Queue everything"]
        direction LR
        N1["15 arrive<br/>per second"] n1@--> N2["Queue grows<br/>5 per second"]
        N2 n2@--> N3["New arrivals wait<br/>30 s at t = 60 s"]
        N3 n3@--> N4["Clients time out<br/>work is wasted"]
    end

    subgraph SHED["Shed the excess"]
        direction LR
        S1["15 arrive<br/>per second"] s1@--> S2["10 admitted<br/>5 rejected fast"]
        S2 s2@--> S3["10 answered<br/>on time"]
        S3 s3@--> S4["5 retry later<br/>or go elsewhere"]
    end

    NOSHED ~~~ SHED
    n1@{ animate: true }
    n2@{ animate: true }
    n3@{ animate: true }
    s1@{ animate: true }
    s2@{ animate: true }
    s3@{ animate: true }

    classDef input fill:#13264a,stroke:#38bdf8,color:#e6f6ff,stroke-width:2px;
    classDef warn fill:#302410,stroke:#fbbf24,color:#fff3cf,stroke-width:2px;
    classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff,stroke-width:2px;
    classDef idle fill:#101e27,stroke:#324950,color:#8aa0a8;
    class N1,S1 input;
    class N2,N3,N4 warn;
    class S2,S3 gpu;
    class S4 idle;</div>
  <figcaption>Top: with no shedding, every request slows down and many time out. Bottom: rejecting the excess immediately keeps the admitted requests fast.</figcaption>
</figure>

<p><strong>Load shedding</strong> means refusing work you cannot finish in time, immediately, so the work you accept stays fast. A quick &ldquo;no&rdquo; is far more useful to a client than a slow timeout: it can retry, fall back to another model, or tell the user right away.</p>
<h3 id="four-shedding-rules-that-work">Four shedding rules that work</h3>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Rule</th>
            <th scope="col">What it does</th>
            <th scope="col">Why it helps</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Bounded queue</td>
            <td>Reject new requests once N are waiting</td>
            <td>Limits backlog; use deadlines to cap waiting time</td>
        </tr>
        <tr>
            <td>Deadline check</td>
            <td>Reject work unlikely to finish before its deadline</td>
            <td>Stops work on answers nobody will read</td>
        </tr>
        <tr>
            <td>Priority classes</td>
            <td>Shed batch traffic before interactive traffic</td>
            <td>Protects the users who are watching</td>
        </tr>
        <tr>
            <td>Output cap</td>
            <td>Enforce a maximum <code>max_tokens</code> per tier</td>
            <td>Stops one request from holding a slot for minutes</td>
        </tr>
    </tbody>
  </table>
</div>
<p>A rough starting point is queue capacity = measured start rate × allowed queue wait. At 10 starts per second and a 2-second queue-wait budget, that gives about 20 waiting requests.</p>
<p>TTFT also includes prefill and other overhead, so leave room for those inside the TTFT target. Variable prompt lengths and batching make this a planning estimate; validate it under load.</p>
<h3 id="say-no-clearly">Say no clearly</h3>
<p>The response matters as much as the decision.</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Situation</th>
            <th scope="col">Status code</th>
            <th scope="col">Meaning to the client</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>This tenant is over its budget</td>
            <td><code>429 Too Many Requests</code></td>
            <td>You are sending too much. Slow down</td>
        </tr>
        <tr>
            <td>The whole server is overloaded</td>
            <td><code>503 Service Unavailable</code></td>
            <td>Not your fault. Try again shortly</td>
        </tr>
    </tbody>
  </table>
</div>
<p>For temporary limits, send a <code>Retry-After</code> header with a realistic wait in seconds. Clients must also support its HTTP-date form. Do not suggest retrying an unchanged request that can never fit the bucket.</p>
<p>The HTTP standards define <a href="https://www.rfc-editor.org/rfc/rfc6585.html#section-4">429 for rate limiting</a> and <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-15.6.4">503 for temporary overload</a>. Both can carry <code>Retry-After</code>; it is a retry hint, not a guarantee of capacity.</p>
<p>On the client side, the matching behavior is short:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">1. Honor Retry-After when it is present
</span></span><span class="line"><span class="cl">2. Otherwise wait, doubling the delay after each failure
</span></span><span class="line"><span class="cl">3. Add a little randomness so clients do not all retry at the same instant
</span></span><span class="line"><span class="cl">4. Give up after a few attempts and tell the user
</span></span></code></pre></div><p>Step 3 is not a detail. Without it, a thousand clients rejected at the same moment all return at the same moment, and the overload repeats.</p>
<h2 id="what-to-watch">What to watch</h2>
<p>Limits you cannot see are limits you cannot tune. Track these three signals on your dashboard:</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Metric to add</th>
            <th scope="col">What it tells you</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Rejections per tenant (429)</td>
            <td>Who is hitting their budget, and how often</td>
        </tr>
        <tr>
            <td>Server-wide rejections (503)</td>
            <td>How often you are out of capacity</td>
        </tr>
        <tr>
            <td>Tokens used versus budget, per tenant</td>
            <td>Who needs a bigger budget, and who has room to spare</td>
        </tr>
    </tbody>
  </table>
</div>
<p>A steady trickle of 429s for one tenant is the system working. A burst of 503s for everyone signals a service-wide problem. Investigate serving health, capacity, and traffic demand before changing the limits.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Mistake</th>
            <th scope="col">What goes wrong</th>
            <th scope="col">Do this instead</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Limiting requests only</td>
            <td>One heavy tenant takes the GPU</td>
            <td>Limit tokens, optionally with a request limit on top</td>
        </tr>
        <tr>
            <td>Counting input tokens only</td>
            <td>Long generations escape the budget</td>
            <td>Reserve <code>max_tokens</code>, then refund</td>
        </tr>
        <tr>
            <td>An unbounded queue</td>
            <td>Latency grows until everything times out</td>
            <td>Bound the queue and reject beyond it</td>
        </tr>
        <tr>
            <td>Rejecting without <code>Retry-After</code></td>
            <td>Clients retry instantly and make it worse</td>
            <td>Send a realistic wait for temporary limits</td>
        </tr>
        <tr>
            <td>One global limit</td>
            <td>Small tenants are punished for a large one</td>
            <td>Budget per tenant</td>
        </tr>
        <tr>
            <td>Per-replica counters</td>
            <td>The real limit is multiplied by the replica count</td>
            <td>Keep counts in a shared store</td>
        </tr>
    </tbody>
  </table>
</div>
<h2 id="cheat-sheet">Cheat sheet</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Rate limiting     = capping how much one tenant may use over time
</span></span><span class="line"><span class="cl">Request limit     = counts calls; useful for bursts but misses differences in cost
</span></span><span class="line"><span class="cl">Token limit       = budgets token volume; a proxy for compute, not a GPU measurement
</span></span><span class="line"><span class="cl">Token bucket      = a budget that refills at a steady rate and allows bounded bursts
</span></span><span class="line"><span class="cl">Refill rate       = the sustained budget, such as 1,000 tokens per second
</span></span><span class="line"><span class="cl">Capacity          = the largest burst the bucket can absorb
</span></span><span class="line"><span class="cl">Reserve and refund = charge input + max_tokens up front, give back what was not used
</span></span><span class="line"><span class="cl">Load shedding     = rejecting work you cannot finish in time so accepted work stays fast
</span></span><span class="line"><span class="cl">Bounded queue     = refuse new requests once the wait would break the latency target
</span></span><span class="line"><span class="cl">429               = this tenant is over budget
</span></span><span class="line"><span class="cl">503               = the whole service is overloaded
</span></span><span class="line"><span class="cl">Retry-After       = tells the client how long to wait before trying again
</span></span><span class="line"><span class="cl">Backoff with jitter = wait longer after each failure, plus randomness to avoid retry storms
</span></span><span class="line"><span class="cl">Rule of thumb     = budget tenants in tokens, bound the queue, and say no quickly
</span></span></code></pre></div><p>Limits decide who gets capacity. For the serving engine behind the gateway, read <a href="https://vishctl.dev/posts/why-vllm-is-dominating-ai-inference-market/">why vLLM is used for inference</a>. For distributing traffic across replicas, read <a href="https://vishctl.dev/posts/llm-d-kubernetes-inference-routing/">LLM inference routing on Kubernetes</a>.</p>
]]></content:encoded></item></channel></rss>