<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: jaycodes</title>
    <description>The latest articles on DEV Community by jaycodes (@janbalangue).</description>
    <link>https://dev.to/janbalangue</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3729482%2Fa15eeb0a-1b2e-48eb-b630-2886e978bc35.png</url>
      <title>DEV Community: jaycodes</title>
      <link>https://dev.to/janbalangue</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/janbalangue"/>
    <language>en</language>
    <item>
      <title>Rate Limiting and Admission Control Solve Different Failure Modes</title>
      <dc:creator>jaycodes</dc:creator>
      <pubDate>Mon, 31 Aug 2026 20:52:43 +0000</pubDate>
      <link>https://dev.to/janbalangue/rate-limiting-and-admission-control-solve-different-failure-modes-k48</link>
      <guid>https://dev.to/janbalangue/rate-limiting-and-admission-control-solve-different-failure-modes-k48</guid>
      <description>&lt;p&gt;Rate limiting is one of the first protections we add to an API.&lt;/p&gt;

&lt;p&gt;A client gets 100 requests per minute. A tenant gets 1,000. Maybe expensive endpoints get tighter limits. When someone exceeds the allowance, the request gets rejected.&lt;/p&gt;

&lt;p&gt;That solves an important problem.&lt;/p&gt;

&lt;p&gt;But it doesn't answer another question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When capacity becomes scarce, which work should actually be allowed to consume it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's where admission control becomes useful.&lt;/p&gt;

&lt;p&gt;The distinction matters increasingly for LLM workloads, where two requests can have radically different costs even though they both count as one request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate limiting controls arrival rate
&lt;/h2&gt;

&lt;p&gt;At a high level, a rate limiter answers something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How much traffic may this caller send during a period of time?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A token bucket might allow 100 requests per minute with some burst capacity.&lt;/p&gt;

&lt;p&gt;That can protect against:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;abusive clients&lt;/li&gt;
&lt;li&gt;accidental retry storms&lt;/li&gt;
&lt;li&gt;noisy tenants&lt;/li&gt;
&lt;li&gt;sudden traffic spikes&lt;/li&gt;
&lt;li&gt;exceeding contractual quotas&lt;/li&gt;
&lt;li&gt;excessive API consumption&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HTTP even has a status code associated with this case. RFC 6585 defines &lt;code&gt;429 Too Many Requests&lt;/code&gt; as indicating that a user has sent too many requests in a given amount of time.&lt;/p&gt;

&lt;p&gt;Rate limiting is extremely useful.&lt;/p&gt;

&lt;p&gt;But rate is only one dimension of resource pressure.&lt;/p&gt;

&lt;h2&gt;
  
  
  A request count isn't a resource model
&lt;/h2&gt;

&lt;p&gt;Imagine an LLM service with a limit of 100 requests per minute.&lt;/p&gt;

&lt;p&gt;Now consider two workloads.&lt;/p&gt;

&lt;p&gt;Interactive requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input:        1,000 tokens
max output:     300 tokens
latency:      user is waiting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Background requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input:       30,000 tokens
max output:   4,000 tokens
latency:      nobody is waiting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both requests count as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 request
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But they aren't equivalent from the perspective of the underlying system.&lt;/p&gt;

&lt;p&gt;They may occupy provider concurrency for different lengths of time. They may consume very different token budgets. They may have completely different latency requirements.&lt;/p&gt;

&lt;p&gt;A rate limiter cannot see that if its unit of accounting is simply requests per second or requests per minute.&lt;/p&gt;

&lt;p&gt;The traffic can therefore remain completely within its configured rate limit while still exhausting a scarce resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concurrency limits help, but introduce another problem
&lt;/h2&gt;

&lt;p&gt;Suppose we add a concurrency limit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;maximum in-flight requests = 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the application can't overwhelm the downstream service with unlimited parallel work.&lt;/p&gt;

&lt;p&gt;That's an improvement.&lt;/p&gt;

&lt;p&gt;But imagine 20 background requests acquire all 20 slots.&lt;/p&gt;

&lt;p&gt;One second later, an interactive request arrives from a user waiting for an answer.&lt;/p&gt;

&lt;p&gt;The rate limiter says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;allowed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The concurrency limiter says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;no capacity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system is protected, but it hasn't necessarily protected the work that matters most.&lt;/p&gt;

&lt;p&gt;We have moved from a rate problem to an allocation problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Admission control asks a different question
&lt;/h2&gt;

&lt;p&gt;In this article, I'm using &lt;strong&gt;admission control&lt;/strong&gt; in the narrower capacity-aware sense.&lt;/p&gt;

&lt;p&gt;Terminology isn't universal. Some systems use "admission control" broadly enough to include rate limiting itself. The useful distinction here is between controlling how quickly traffic may arrive and deciding whether a particular request should consume currently scarce execution capacity.&lt;/p&gt;

&lt;p&gt;Under that definition, admission control asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Given the capacity available right now, should this specific piece of work be allowed to start?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That decision can incorporate more information than a conventional rate limiter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;current concurrency
current resource utilization
estimated request cost
request priority
tenant
workload class
reserved capacity
queue depth
deadlines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of merely counting arrivals, we're deciding how scarce capacity should be allocated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consider interactive and batch traffic
&lt;/h2&gt;

&lt;p&gt;Suppose a service has 32 execution slots.&lt;/p&gt;

&lt;p&gt;Two workload classes share them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interactive
batch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without additional controls, batch processing may consume all 32 slots.&lt;/p&gt;

&lt;p&gt;A concurrency limiter still prevents the system from exceeding 32 requests, but interactive traffic now waits behind work that nobody is waiting for.&lt;/p&gt;

&lt;p&gt;One alternative is to statically divide capacity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interactive: 28 slots
batch:        4 slots
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That protects interactive traffic, but it can waste capacity.&lt;/p&gt;

&lt;p&gt;If only 10 interactive requests are running, 18 interactive slots sit idle while batch work waits.&lt;/p&gt;

&lt;p&gt;A more flexible admission policy could instead say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Interactive traffic has protected capacity.

Batch traffic may borrow unused capacity.

When interactive demand increases, new batch admissions stop
until protected capacity is restored.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the system can simultaneously pursue two goals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;keep expensive infrastructure utilized when capacity is available&lt;/li&gt;
&lt;li&gt;protect latency-sensitive work when contention appears&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A request-per-minute limit alone cannot express that policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  LLM workloads make the distinction more obvious
&lt;/h2&gt;

&lt;p&gt;This problem exists in ordinary distributed systems, but LLM APIs make it particularly visible.&lt;/p&gt;

&lt;p&gt;Request cost varies dramatically.&lt;/p&gt;

&lt;p&gt;A request containing a short chat message isn't equivalent to a request asking a model to process a large document with a large maximum output budget.&lt;/p&gt;

&lt;p&gt;So an LLM admission controller might track both concurrency and an approximate in-flight token budget.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request A:
input estimate      = 1,200
max output          =   400
reserved budget     = 1,600

request B:
input estimate      = 24,000
max output          = 3,000
reserved budget     = 27,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the admission decision can consider resource pressure rather than only the request count.&lt;/p&gt;

&lt;p&gt;The reservation doesn't even need to perfectly predict final token usage to be useful.&lt;/p&gt;

&lt;p&gt;It can reserve conservatively at admission and reconcile the reservation once actual usage is known.&lt;/p&gt;

&lt;p&gt;That turns admission into a resource-allocation problem rather than simply a traffic-counting problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate limiting can also miss overload that has already started
&lt;/h2&gt;

&lt;p&gt;There is another important difference.&lt;/p&gt;

&lt;p&gt;A rate limit generally represents a policy about incoming traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tenant A may send 50 requests/second
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the safe rate of a distributed system isn't necessarily constant.&lt;/p&gt;

&lt;p&gt;Maybe a downstream provider has slowed down.&lt;/p&gt;

&lt;p&gt;Requests that normally complete in 500 ms now take 8 seconds.&lt;/p&gt;

&lt;p&gt;Even if the arrival rate hasn't changed, concurrency begins accumulating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;arrival rate stays constant
        ↓
requests take longer
        ↓
in-flight work grows
        ↓
queues grow
        ↓
latency rises
        ↓
timeouts trigger retries
        ↓
even more work arrives
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Google's SRE guidance explicitly warns that simple rate limiting may not account for overall service health and therefore may not stop a failure that has already begun. It recommends rejecting work as systems approach overload and shedding load before resource exhaustion produces cascading failures.&lt;/p&gt;

&lt;p&gt;This is a different failure mode from a client merely sending too many requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  They belong together
&lt;/h2&gt;

&lt;p&gt;The lesson isn't:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Replace rate limiting with admission control.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use each mechanism for the failure mode it is good at controlling.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A production path might look roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request
   │
   ▼
authentication
   │
   ▼
rate limit / quota
   │
   ▼
admission control
   │
   ▼
downstream service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;rateLimiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;tooManyRequests&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reservation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;admissionController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tryAcquire&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;workloadClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workloadClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;estimatedCost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;estimateCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;reservation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;overloaded&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;callDownstream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;reservation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rate limiter protects the service from traffic policy violations.&lt;/p&gt;

&lt;p&gt;The admission controller protects scarce execution capacity.&lt;/p&gt;

&lt;p&gt;Those aren't identical jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Different failure modes, different questions
&lt;/h2&gt;

&lt;p&gt;I find it useful to frame the difference this way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting asks:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How much traffic may this caller send?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Concurrency limiting asks:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How much work may execute simultaneously?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Admission control asks:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which work should consume scarce capacity right now?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Load shedding asks:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which work should we stop accepting because the system is approaching overload?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These mechanisms overlap, and real systems frequently combine them. The boundaries aren't perfectly clean.&lt;/p&gt;

&lt;p&gt;But the questions they answer are different enough that treating all of them as "rate limiting" can hide important design decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for agents
&lt;/h2&gt;

&lt;p&gt;Agentic systems make the allocation problem even more interesting.&lt;/p&gt;

&lt;p&gt;A single user action can create multiple downstream model calls.&lt;/p&gt;

&lt;p&gt;Background agents may execute continuously.&lt;/p&gt;

&lt;p&gt;Retries can multiply requests.&lt;/p&gt;

&lt;p&gt;Tool calls may produce additional model calls.&lt;/p&gt;

&lt;p&gt;Long-context operations can consume much more capacity than short interactive requests.&lt;/p&gt;

&lt;p&gt;So eventually the question stops being:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How many requests per minute should we allow?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;When demand exceeds available capacity,
which work gets to continue?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a scheduling and resource-allocation question.&lt;/p&gt;

&lt;p&gt;Rate limiting alone doesn't answer it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger reliability lesson
&lt;/h2&gt;

&lt;p&gt;Overload doesn't always look like a crash.&lt;/p&gt;

&lt;p&gt;Sometimes every component remains technically healthy while the wrong work consumes the available capacity.&lt;/p&gt;

&lt;p&gt;Queues grow.&lt;/p&gt;

&lt;p&gt;Interactive requests wait behind batch jobs.&lt;/p&gt;

&lt;p&gt;Retries increase pressure.&lt;/p&gt;

&lt;p&gt;Latency explodes.&lt;/p&gt;

&lt;p&gt;Eventually, users experience a failure even though the system is still processing requests exactly as designed.&lt;/p&gt;

&lt;p&gt;Reliable systems, therefore, need more than a maximum request rate.&lt;/p&gt;

&lt;p&gt;They need a policy for scarcity.&lt;/p&gt;

&lt;p&gt;That is the problem admission control is trying to solve.&lt;/p&gt;




&lt;p&gt;I've been exploring this problem while building &lt;code&gt;async-bulkhead-llm&lt;/code&gt; and MoFlux, particularly around token-aware admission and protecting interactive traffic while allowing lower-priority workloads to use otherwise idle capacity.&lt;/p&gt;

&lt;p&gt;The deeper I get into the problem, the more useful this distinction becomes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting controls how much traffic arrives. Admission control decides which work deserves scarce capacity when it does.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>distributedsystems</category>
      <category>performance</category>
    </item>
    <item>
      <title>When Better Latency Metrics Hide Worse Reliability</title>
      <dc:creator>jaycodes</dc:creator>
      <pubDate>Sat, 29 Aug 2026 17:36:23 +0000</pubDate>
      <link>https://dev.to/janbalangue/when-better-latency-metrics-hide-worse-reliability-42ob</link>
      <guid>https://dev.to/janbalangue/when-better-latency-metrics-hide-worse-reliability-42ob</guid>
      <description>&lt;p&gt;A system completes 93% of interactive requests with a p95 latency of 10 seconds.&lt;/p&gt;

&lt;p&gt;Another completes 97% with a p95 latency of 12 seconds.&lt;/p&gt;

&lt;p&gt;Which one performed better?&lt;/p&gt;

&lt;p&gt;The obvious answer seems to be the first system. Twelve seconds is worse than ten.&lt;/p&gt;

&lt;p&gt;But there is a problem hidden in that comparison:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The latency percentile is calculated only over requests that succeeded.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The second system completed requests that the first system did not.&lt;/p&gt;

&lt;p&gt;Once systems admit, reject, retry, or shed different subsets of traffic, successful-request latency stops being an apples-to-apples comparison by itself.&lt;/p&gt;

&lt;p&gt;This matters particularly under overload.&lt;/p&gt;

&lt;p&gt;And it matters a lot for LLM infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The denominator changed
&lt;/h2&gt;

&lt;p&gt;Latency percentiles are extremely useful.&lt;/p&gt;

&lt;p&gt;Google's SRE guidance recommends percentiles precisely because averages can obscure the long tail of request latency. A system with a reasonable average can still have a terrible experience for its slowest requests.&lt;/p&gt;

&lt;p&gt;But every percentile has a population.&lt;/p&gt;

&lt;p&gt;Consider 100 requests sent to two systems.&lt;/p&gt;

&lt;p&gt;System A completes 90:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Requests offered:     100
Requests completed:    90
Requests failed:       10
Successful p95:       4.0s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;System B completes 99:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Requests offered:     100
Requests completed:    99
Requests failed:        1
Successful p95:       5.0s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looking only at latency produces a simple conclusion:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;System A is faster.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Looking only at successful completion produces another:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;System B is more reliable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Neither metric is wrong.&lt;/p&gt;

&lt;p&gt;They measure different properties.&lt;/p&gt;

&lt;p&gt;The mistake is turning either one into a complete description of the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overload makes this especially dangerous
&lt;/h2&gt;

&lt;p&gt;Under ordinary load, two systems may successfully process almost exactly the same requests.&lt;/p&gt;

&lt;p&gt;Their latency distributions are therefore reasonably comparable.&lt;/p&gt;

&lt;p&gt;Under overload, admission policy starts changing the population.&lt;/p&gt;

&lt;p&gt;A system can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reject requests immediately;&lt;/li&gt;
&lt;li&gt;queue them;&lt;/li&gt;
&lt;li&gt;retry them;&lt;/li&gt;
&lt;li&gt;prioritize one workload class;&lt;/li&gt;
&lt;li&gt;shed expensive work;&lt;/li&gt;
&lt;li&gt;reserve capacity for selected traffic;&lt;/li&gt;
&lt;li&gt;allow requests through until an upstream dependency rejects them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those decisions determine which requests eventually appear in the successful-request latency histogram.&lt;/p&gt;

&lt;p&gt;That means the latency measurement becomes conditional:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency, given that the request succeeded.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is not the same metric as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happened to a request offered to the system?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The distinction becomes important as failure rates diverge.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example
&lt;/h2&gt;

&lt;p&gt;I ran into this while evaluating admission-control strategies for mixed LLM workloads.&lt;/p&gt;

&lt;p&gt;The benchmark compared four approaches under the same deterministic workload:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No application-level control&lt;/li&gt;
&lt;li&gt;A static cap&lt;/li&gt;
&lt;li&gt;Redis-coordinated admission&lt;/li&gt;
&lt;li&gt;Local admission backed by distributed capacity coordination&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The workload intentionally exceeded the provider's 32-request capacity envelope and mixed latency-sensitive interactive requests with background batch work.&lt;/p&gt;

&lt;p&gt;At the zero-coordinator-latency rung, measured across eight matched seeds, two results initially seemed contradictory.&lt;/p&gt;

&lt;p&gt;Redis produced better successful-request latency:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Redis&lt;/th&gt;
&lt;th&gt;Local coordinated admission&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Interactive p50&lt;/td&gt;
&lt;td&gt;4.22s&lt;/td&gt;
&lt;td&gt;5.25s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interactive p95&lt;/td&gt;
&lt;td&gt;10.30s&lt;/td&gt;
&lt;td&gt;11.54s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT p50&lt;/td&gt;
&lt;td&gt;0.79s&lt;/td&gt;
&lt;td&gt;1.04s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFT p95&lt;/td&gt;
&lt;td&gt;2.92s&lt;/td&gt;
&lt;td&gt;5.46s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If I stopped there, Redis would clearly look better.&lt;/p&gt;

&lt;p&gt;But the completion results looked different:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Redis&lt;/th&gt;
&lt;th&gt;Local coordinated admission&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Interactive success&lt;/td&gt;
&lt;td&gt;92.7%&lt;/td&gt;
&lt;td&gt;97.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interactive goodput&lt;/td&gt;
&lt;td&gt;5.50 req/s&lt;/td&gt;
&lt;td&gt;5.73 req/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry amplification&lt;/td&gt;
&lt;td&gt;1.478×&lt;/td&gt;
&lt;td&gt;1.339×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local rejects&lt;/td&gt;
&lt;td&gt;306.5&lt;/td&gt;
&lt;td&gt;264&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The local strategy completed substantially more interactive work while performing fewer retries and shedding fewer attempts.&lt;/p&gt;

&lt;p&gt;Redis completed fewer interactive logical requests, but the requests that &lt;em&gt;did&lt;/em&gt; complete reached their first token faster.&lt;/p&gt;

&lt;p&gt;Both observations are true.&lt;/p&gt;

&lt;h2&gt;
  
  
  So which system actually won?
&lt;/h2&gt;

&lt;p&gt;That question is too broad.&lt;/p&gt;

&lt;p&gt;Redis won one dimension of the experiment:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;responsiveness among successfully completed requests.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The local admission strategy won another:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;successful completion of protected interactive work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is tempting to collapse those into a single ranking.&lt;/p&gt;

&lt;p&gt;Doing so throws away useful information.&lt;/p&gt;

&lt;p&gt;If your primary objective is minimizing latency for requests that survive overload, the Redis result may be preferable.&lt;/p&gt;

&lt;p&gt;If your primary objective is preserving interactive completion when capacity becomes scarce, the other policy may be preferable.&lt;/p&gt;

&lt;p&gt;And if batch throughput matters most, the answer changes again: Redis also completed considerably more batch work in this particular benchmark.&lt;/p&gt;

&lt;p&gt;The benchmark therefore does not establish a universal winner.&lt;/p&gt;

&lt;p&gt;It exposes a tradeoff.&lt;/p&gt;

&lt;p&gt;That is exactly what a useful benchmark should do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The survivor effect
&lt;/h2&gt;

&lt;p&gt;There is a deeper measurement issue here.&lt;/p&gt;

&lt;p&gt;Imagine a workload with ten requests.&lt;/p&gt;

&lt;p&gt;Their hypothetical service times, if all could run successfully, might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1s
1s
1s
2s
2s
2s
3s
5s
8s
15s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine System A admits all ten.&lt;/p&gt;

&lt;p&gt;System B admits the first seven and rejects the last three.&lt;/p&gt;

&lt;p&gt;System B's successful-request latency distribution looks excellent.&lt;/p&gt;

&lt;p&gt;But it did not make the 5-, 8-, and 15-second requests faster.&lt;/p&gt;

&lt;p&gt;It removed them from the population being measured.&lt;/p&gt;

&lt;p&gt;This is an intentionally simplified example, but it demonstrates the problem.&lt;/p&gt;

&lt;p&gt;A successful-request latency percentile tells you about the survivors.&lt;/p&gt;

&lt;p&gt;It does not tell you what happened to the requests missing from the sample.&lt;/p&gt;

&lt;p&gt;This resembles a familiar statistical problem: conditioning on an outcome can change the composition of the population you are comparing.&lt;/p&gt;

&lt;p&gt;In infrastructure benchmarks, admission and load shedding can create exactly that situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be careful with the stronger interpretation
&lt;/h2&gt;

&lt;p&gt;There is a tempting explanation for the benchmark above:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The local strategy had worse p95 latency because it successfully carried harder requests that Redis rejected.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is plausible.&lt;/p&gt;

&lt;p&gt;It is not fully established by the aggregate numbers.&lt;/p&gt;

&lt;p&gt;The benchmark did include requests with substantially different sizes—interactive request sizes varied by roughly 25× to 32×—and token pressure was a real admission constraint. The two systems also made different rejection decisions.&lt;/p&gt;

&lt;p&gt;But proving the stronger statement would require matched request-level counterfactual evidence.&lt;/p&gt;

&lt;p&gt;For each request rejected by one policy and completed by another, we would want to know what its latency would have been had the rejecting system admitted it.&lt;/p&gt;

&lt;p&gt;We cannot observe both outcomes in the same run.&lt;/p&gt;

&lt;p&gt;So the defensible conclusion is narrower:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When completion rates differ, successful-request latency percentiles describe different populations and therefore should not be interpreted independently of completion.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That claim does not require speculation.&lt;/p&gt;

&lt;p&gt;The measurements themselves establish it.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is why availability and latency belong together
&lt;/h2&gt;

&lt;p&gt;SRE practice already points in this direction.&lt;/p&gt;

&lt;p&gt;Google treats both availability and latency as distinct service-level indicators. Availability measures the proportion of requests that succeed; latency measures how quickly requests are served.&lt;/p&gt;

&lt;p&gt;Neither replaces the other.&lt;/p&gt;

&lt;p&gt;Suppose a release changes your dashboard from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Success: 99.5%
p95:     800ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Success: 94.0%
p95:     500ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Celebrating the 300ms latency improvement would be premature.&lt;/p&gt;

&lt;p&gt;Maybe the service became dramatically more efficient.&lt;/p&gt;

&lt;p&gt;Or maybe it stopped serving a meaningful portion of its workload.&lt;/p&gt;

&lt;p&gt;You have to investigate both.&lt;/p&gt;

&lt;p&gt;The same principle applies when comparing architectures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries distort the picture further
&lt;/h2&gt;

&lt;p&gt;Retries introduce another layer.&lt;/p&gt;

&lt;p&gt;Suppose a logical user request is rejected twice and succeeds on its third attempt.&lt;/p&gt;

&lt;p&gt;At the attempt level, you may observe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attempt 1: rejected
Attempt 2: rejected
Attempt 3: TTFT 900ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the dashboard reports only the final successful provider attempt, the user appears to have received a response with a 900ms time to first token.&lt;/p&gt;

&lt;p&gt;They didn't.&lt;/p&gt;

&lt;p&gt;They experienced rejection, backoff, another rejection, more backoff, and then 900ms to first token.&lt;/p&gt;

&lt;p&gt;A benchmark needs to distinguish at least three concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;provider-attempt latency;&lt;/li&gt;
&lt;li&gt;successful logical-request latency;&lt;/li&gt;
&lt;li&gt;end-to-end user-observed latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Otherwise retry-heavy systems can appear healthier than they are.&lt;/p&gt;

&lt;p&gt;This matters operationally as well as statistically. Google warns that retries can amplify overload and contribute to cascading failure, which is why exponential backoff and jitter are standard overload protections.&lt;/p&gt;

&lt;p&gt;In the LLM benchmark, this was one reason I tracked retry amplification alongside latency.&lt;/p&gt;

&lt;p&gt;Redis produced lower successful-request TTFT, but required 1.478 attempts per logical interactive request at the median.&lt;/p&gt;

&lt;p&gt;The local strategy recorded 1.339.&lt;/p&gt;

&lt;p&gt;Neither number means much without the other metrics around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rejections need context too
&lt;/h2&gt;

&lt;p&gt;Even rejection count can mislead.&lt;/p&gt;

&lt;p&gt;One admission controller may reject an attempt locally before expensive work begins.&lt;/p&gt;

&lt;p&gt;Another may admit that attempt, consume provider capacity, and eventually receive a provider-side 429.&lt;/p&gt;

&lt;p&gt;A third may queue the request until the caller times out.&lt;/p&gt;

&lt;p&gt;All three can result in an unsuccessful request.&lt;/p&gt;

&lt;p&gt;Operationally, however, they are very different outcomes.&lt;/p&gt;

&lt;p&gt;Early rejection is often deliberate overload protection.&lt;/p&gt;

&lt;p&gt;Google's SRE guidance recommends graceful load shedding when a service exceeds sustainable capacity rather than allowing overload to turn into extreme latency or cascading failure.&lt;/p&gt;

&lt;p&gt;So "fewer rejects" is not automatically better either.&lt;/p&gt;

&lt;p&gt;What ultimately matters is what happens to the &lt;strong&gt;logical workload&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Was the important request eventually completed?&lt;/p&gt;

&lt;p&gt;How long did the caller wait?&lt;/p&gt;

&lt;p&gt;How many attempts did it require?&lt;/p&gt;

&lt;p&gt;How much capacity was consumed along the way?&lt;/p&gt;

&lt;p&gt;What other work was displaced?&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a scorecard, not a hero metric
&lt;/h2&gt;

&lt;p&gt;For overload experiments, I now prefer to think in terms of a scorecard.&lt;/p&gt;

&lt;p&gt;For latency-sensitive traffic, I want at least:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Offered logical requests
        |
        +-- completed
        |
        +-- exhausted / failed
        |
        +-- attempts
              |
              +-- admitted
              +-- locally rejected
              +-- upstream rejected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then I measure:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Completion&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;logical-request success rate;&lt;/li&gt;
&lt;li&gt;successful goodput;&lt;/li&gt;
&lt;li&gt;exhaustion rate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;User experience&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;end-to-end p50;&lt;/li&gt;
&lt;li&gt;end-to-end p95;&lt;/li&gt;
&lt;li&gt;TTFT p50;&lt;/li&gt;
&lt;li&gt;TTFT p95.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Admission behavior&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local rejects;&lt;/li&gt;
&lt;li&gt;queue wait;&lt;/li&gt;
&lt;li&gt;token-budget rejects;&lt;/li&gt;
&lt;li&gt;concurrency-bound rejects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Amplification&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;attempts per logical request;&lt;/li&gt;
&lt;li&gt;retries per request;&lt;/li&gt;
&lt;li&gt;upstream 429s.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Capacity&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;peak occupancy;&lt;/li&gt;
&lt;li&gt;in-flight concurrency;&lt;/li&gt;
&lt;li&gt;in-flight token exposure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And for mixed workloads, every important result should be segmented by workload class.&lt;/p&gt;

&lt;p&gt;An aggregate 95% success rate is not reassuring if interactive success is 80% and low-priority batch success is nearly 100%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Always print the denominator beside the percentile
&lt;/h2&gt;

&lt;p&gt;A simple reporting habit prevents a surprising amount of confusion.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TTFT p95: 2.9s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Interactive success: 92.7%
Successful-request TTFT p95: 2.9s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Offered:                   270 logical requests
Completed:                 250
Interactive success:       92.7%
Successful-request p95:    10.3s
Successful-request TTFT:    2.9s
Attempts/logical request:   1.478
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the reader can see the population behind the latency statistic.&lt;/p&gt;

&lt;p&gt;The word &lt;strong&gt;successful-request&lt;/strong&gt; is worth keeping.&lt;/p&gt;

&lt;p&gt;It is a reminder that failures are absent from the percentile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Matched workloads matter
&lt;/h2&gt;

&lt;p&gt;The measurement problem becomes worse when benchmark workloads themselves differ.&lt;/p&gt;

&lt;p&gt;If one system receives smaller requests, fewer long prompts, or a different arrival sequence, latency comparisons tell you very little.&lt;/p&gt;

&lt;p&gt;This is why controlled overload experiments should use matched workloads.&lt;/p&gt;

&lt;p&gt;In the benchmark described above, each strategy was run against matched seeds, and results were reported across eight seeds rather than selecting one representative-looking execution. The broader coordinator-distance experiment ran eight matched seeds at four distance rungs, producing 32 recorded proof combinations.&lt;/p&gt;

&lt;p&gt;Repeating runs matters because overload is timing-sensitive.&lt;/p&gt;

&lt;p&gt;Queue ordering changes.&lt;/p&gt;

&lt;p&gt;Retries shift arrival patterns.&lt;/p&gt;

&lt;p&gt;Capacity becomes available at slightly different moments.&lt;/p&gt;

&lt;p&gt;A single run can produce a compelling graph that turns out not to represent the system reliably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't optimize the dashboard
&lt;/h2&gt;

&lt;p&gt;There is a dangerous feedback loop in performance engineering:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose an important metric.&lt;/li&gt;
&lt;li&gt;Optimize the system.&lt;/li&gt;
&lt;li&gt;Watch the metric improve.&lt;/li&gt;
&lt;li&gt;Assume the system improved.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Usually that works.&lt;/p&gt;

&lt;p&gt;Sometimes the optimization changes what gets counted.&lt;/p&gt;

&lt;p&gt;Admission control is a particularly clear example.&lt;/p&gt;

&lt;p&gt;A stricter policy can improve the latency distribution simply by excluding more work.&lt;/p&gt;

&lt;p&gt;A more permissive policy can make latency look worse while completing requests that would otherwise disappear from the successful population.&lt;/p&gt;

&lt;p&gt;Neither outcome is inherently desirable.&lt;/p&gt;

&lt;p&gt;The business requirement determines which tradeoff is acceptable.&lt;/p&gt;

&lt;p&gt;If a user clicks "Generate" in an interactive application, I might willingly accept an additional second of tail latency to materially improve the probability that the request completes.&lt;/p&gt;

&lt;p&gt;For a latency-sensitive trading system, that trade might be unacceptable.&lt;/p&gt;

&lt;p&gt;For an asynchronous document-processing pipeline, throughput might dominate both.&lt;/p&gt;

&lt;p&gt;Metrics don't decide this.&lt;/p&gt;

&lt;p&gt;They tell us what trade we made.&lt;/p&gt;

&lt;h2&gt;
  
  
  A better question
&lt;/h2&gt;

&lt;p&gt;When comparing overloaded systems, don't ask only:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which one has the lower p95?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lower p95 over which requests?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happened to the rest?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That small change in framing makes overload benchmarks much harder to misread.&lt;/p&gt;

&lt;p&gt;Latency percentiles remain indispensable. Tail latency matters, and averages absolutely can hide serious performance problems.&lt;/p&gt;

&lt;p&gt;But latency is only one part of the outcome.&lt;/p&gt;

&lt;p&gt;Once two systems make different admission, retry, or load-shedding decisions, their successful-request distributions may no longer contain equivalent populations.&lt;/p&gt;

&lt;p&gt;At that point, a prettier latency number can coexist with a worse reliability result.&lt;/p&gt;

&lt;p&gt;The percentile isn't lying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We're just asking it to tell us something it doesn't measure.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>latency</category>
      <category>reliability</category>
      <category>ai</category>
    </item>
    <item>
      <title>When batch traffic competes with interactive LLM requests</title>
      <dc:creator>jaycodes</dc:creator>
      <pubDate>Thu, 27 Aug 2026 22:12:00 +0000</pubDate>
      <link>https://dev.to/janbalangue/when-batch-traffic-competes-with-interactive-llm-requests-5684</link>
      <guid>https://dev.to/janbalangue/when-batch-traffic-competes-with-interactive-llm-requests-5684</guid>
      <description>&lt;p&gt;Not all LLM traffic is equally urgent. &lt;/p&gt;

&lt;p&gt;A user waiting for a response cares about the next few seconds, whereas a batch task summarizing thousands of documents usually doesn't.&lt;/p&gt;

&lt;p&gt;If both types of work share the same capacity, your infrastructure may treat them the same.&lt;/p&gt;

&lt;p&gt;Here's how that can become a problem. &lt;/p&gt;

&lt;p&gt;Imagine your application handles two kinds of workloads: interactive (user waiting for a response) and batch (background summarization, classification, extraction, or enrichment jobs). Both call the same LLM provider. &lt;/p&gt;

&lt;p&gt;When batch processing ramps up, suddenly your interactive requests have to compete with batch traffic that nobody is waiting for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try limiting concurrency
&lt;/h2&gt;

&lt;p&gt;One obvious solution is to limit the LLM concurrency. However, a concurrency limit knows how many requests are running. It has no idea which requests matter most. So if 20 batch requests acquire all 20 slots, latency-sensitive interactive traffic has nowhere to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queueing doesn't necessarily solve it
&lt;/h2&gt;

&lt;p&gt;What about a queue for excess requests? In that case, the interactive request waits behind the batch traffic. Technically, the system is behaving correctly, but the user experience is terrible.&lt;/p&gt;

&lt;p&gt;This is one way overload can be deceptive. No crashes, but requests spend more time waiting for capacity. TTFT rises, tail latency worsens, callers time out or retry, retries create more work.&lt;/p&gt;

&lt;p&gt;Resource contention can turn into an overload feedback loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  LLM workloads worsen the problem
&lt;/h2&gt;

&lt;p&gt;But it's not just concurrency. Let's consider two requests:&lt;/p&gt;

&lt;p&gt;Interactive:&lt;br&gt;
1,000 input tokens&lt;br&gt;
500 maximum output tokens&lt;/p&gt;

&lt;p&gt;Batch:&lt;br&gt;
30,000 input tokens&lt;br&gt;
4,000 maximum output tokens&lt;/p&gt;

&lt;p&gt;Each consumes one concurrency slot, but they aren't anywhere near the same amount of work.&lt;/p&gt;

&lt;p&gt;This is why it's useful to consider both concurrent requests and in-flight token commitments in making admission decisions. Before starting a request, estimate its potential token footprint and reserve that amount. It doesn't have to be exact; a reasonable approximation can still prevent a small number of very large requests from consuming a disproportionate amount of capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Priority alone isn't enough
&lt;/h2&gt;

&lt;p&gt;You could also prioritize interactive requests, which helps when multiple requests are waiting. However, priority cannot recover capacity already tied up in running requests. If batch work occupies every slot, a high-priority request would still be stuck waiting.&lt;/p&gt;

&lt;p&gt;It's important not only to ask which request should run next, but also whether a request should be allowed to consume capacity now. That's why admission control matters.&lt;/p&gt;

&lt;p&gt;One approach is to reserve part of the available capacity for different workload classes. If you have 20 available concurrency slots, try something like the following:&lt;/p&gt;

&lt;p&gt;Interactive protected capacity: 8&lt;br&gt;
Batch protected capacity:       2&lt;br&gt;
Shared capacity:               10&lt;/p&gt;

&lt;p&gt;These numbers can vary depending on workload requirements. The point is that protected capacity can prevent either workload from starving the other. Interactive does not always win. &lt;/p&gt;

&lt;p&gt;The goal is to protect latency-sensitive work, ensure background processes run, and share the remaining capacity efficiently. This is workload isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fail fast can be better than fail slowly
&lt;/h2&gt;

&lt;p&gt;When capacity isn't available, there are often two choices: wait or reject. Waiting sounds friendlier, but if requests accumulate faster than the system can complete them, queueing converts overload into latency.&lt;/p&gt;

&lt;p&gt;Sometimes the more reliable behavior is to reject work immediately.&lt;/p&gt;

&lt;p&gt;The broader lesson is that overload management encompasses more than limiting how much work enters the system. It's also about deciding which work is allowed to proceed when demand exceeds system capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;async-bulkhead-llm README — fail-fast admission control, concurrency limits, token budgeting, reconciliation, telemetry, and overload protection.&lt;/li&gt;
&lt;li&gt;async-bulkhead-llm: Admission classes — protected floors, ceilings, shared capacity, borrowing, and class telemetry.&lt;/li&gt;
&lt;li&gt;llm-d: Batch Serving — discussion of co-locating offline/batch inference with real-time workloads and protecting interactive traffic from batch contention.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>node</category>
      <category>performance</category>
    </item>
    <item>
      <title>Concurrency Limits Aren’t Enough for LLM APIs</title>
      <dc:creator>jaycodes</dc:creator>
      <pubDate>Tue, 25 Aug 2026 03:27:59 +0000</pubDate>
      <link>https://dev.to/janbalangue/concurrency-limits-arent-enough-for-llm-apis-4lgm</link>
      <guid>https://dev.to/janbalangue/concurrency-limits-arent-enough-for-llm-apis-4lgm</guid>
      <description>&lt;p&gt;A concurrency limit treats every request as roughly equal.&lt;/p&gt;

&lt;p&gt;For LLM workloads, that assumption breaks down.&lt;/p&gt;

&lt;p&gt;A request with a 500-token budget and one with a 30,000-token budget might both occupy a single concurrency slot, but they can represent very different amounts of provider capacity.&lt;/p&gt;

&lt;p&gt;That’s the problem I built async-bulkhead-llm to address.&lt;/p&gt;

&lt;p&gt;Instead of limiting only concurrent requests, it can bound two things at once:&lt;/p&gt;

&lt;p&gt;concurrent requests&lt;br&gt;
+&lt;br&gt;
estimated in-flight tokens&lt;/p&gt;

&lt;p&gt;A request is admitted only when both budgets have enough capacity.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bulkhead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AsyncBulkhead&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;maxConcurrent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxInFlightTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a bulkhead around an LLM provider that accounts for workload size rather than simply request count.&lt;/p&gt;

&lt;p&gt;This becomes especially useful when interactive and large batch requests share the same upstream model. A conventional concurrency limiter can still allow a few very large requests to consume most of the useful capacity.&lt;/p&gt;

&lt;p&gt;Token-aware admission gives you another control surface.&lt;/p&gt;

&lt;p&gt;async-bulkhead-llm is an open-source TypeScript/npm library I’ve been developing specifically around this problem.&lt;/p&gt;

&lt;p&gt;The broader lesson has been simple: for LLM systems, concurrency is only one dimension of load.&lt;/p&gt;

&lt;p&gt;If you're building production LLM infrastructure, I'd be interested in how you're handling admission control and overload protection.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
