SIVARO
GPU Cluster Management

Admission Control vs Circuit Breaker: The LLM Difference That Saves Your Inference Server

We were three weeks into production with a customer-facing LLM feature at SIVARO. The model was fine. The prompts were fine. Then a marketing email went out,...

admissioncontrolcircuitbreakerdifferencethatsavesyour
By Nishaant Dixit
Admission Control vs Circuit Breaker: The LLM Difference That Saves Your Inference Server

Admission Control vs Circuit Breaker: The LLM Difference That Saves Your Inference Server

Free Technical Audit

Expert Review

Get Started →
Admission Control vs Circuit Breaker: The LLM Difference That Saves Your Inference Server

We were three weeks into production with a customer-facing LLM feature at SIVARO. The model was fine. The prompts were fine. Then a marketing email went out, and our vLLM inference cluster started returning 429s so fast the retry storms took down the ingress controller. Not the model. The request path.

That afternoon I sat with our infra lead and we argued about whether we needed "better autoscaling" or "a circuit breaker." Turns out we needed neither. We needed admission control. And that distinction — admission control vs circuit breaker — is the difference between protecting your system from too much and rescuing it from already failing.

Let me show you what I mean.

Admission control circuit breaker difference llm comes down to one question: are you preventing overload or responding to failure? Most teams conflate them. They deploy one when they need the other. And when you're running production AI systems, that confusion costs you real money and real trust.

Here's what I've learned running inference infrastructure for clients since 2023, and what we now bake into every LLM deployment we touch.


What Admission Control Actually Does

Admission control decides whether a request gets to enter the system at all. It's a gate. Not a queue. Not a retry mechanism. A gate.

For a vLLM inference server, that gate sits in front of the model. It looks at current load — queue depth, active requests, GPU utilization, token generation speed — and makes a binary decision: admit or reject.

Rejection here doesn't mean "try again later." It means "the system is saturated and your request will degrade everyone else's experience if we let it in."

You can't autoscale your way out of this. I've seen teams try. They spin up more replicas, but the bottleneck isn't replicas. It's the shared KV cache memory on the GPUs, or the fact that your model is generating tokens at 45 tokens per second and no amount of horizontal scaling fixes a single-sequence latency problem.

Here's a practical admission control config for vLLM:

python
from vllm.engine.arg_utils import AsyncEngineArgs
from vllm import AsyncLLMEngine

engine_args = AsyncEngineArgs(
    model="meta-llama/Llama-3.1-8B-Instruct",
    max_num_seqs=64,           # Maximum concurrent sequences
    max_num_batched_tokens=8192,  # Token budget per batch
    gpu_memory_utilization=0.90,
    enable_prefix_caching=True,
)

Those max_num_seqs and max_num_batched_tokens values are admission control. When the engine hits those limits, it stops accepting new requests. Requests wait in the client or get rejected.

The problem? Default client behavior is to retry. And retries at scale become a self-inflicted DDoS.


Circuit Breakers: What They're Really For

A circuit breaker is different. It doesn't prevent overload by design — it detects that something has already gone wrong and stops making it worse by halting requests temporarily.

Think of the electrical analogy. When current spikes, the breaker trips. You don't ask "why did the breaker not prevent the spike?" That's not its job. Its job is to stop the damage once the spike happens.

In LLM inference, circuit breakers protect against:

  • Hung requests — the model is alive but not generating (deadlock in the batch scheduler)
  • Error cascades — one bad request pattern causes the engine to crash-loop
  • Upstream failures — your embedding model or retrieval service is down, and every request to the LLM will fail anyway

Most teams I talk to think circuit breakers are about latency. They set a timeout threshold and trip when p95 exceeds it. That's not a circuit breaker. That's a slow-loris detector.

A real circuit breaker for LLM inference tracks failure rate, not latency. Here's the pattern:

python
import time
from collections import deque

class LLMCircuitBreaker:
    def __init__(self, failure_threshold=0.5, window_seconds=60, cooldown_seconds=30):
        self.failure_threshold = failure_threshold
        self.window_seconds = window_seconds
        self.cooldown_seconds = cooldown_seconds
        self.failures = deque()
        self.total = deque()
        self.state = "closed"  # closed, open, half-open
        self.opened_at = None

    def record(self, success):
        now = time.time()
        self.total.append(now)
        if not success:
            self.failures.append(now)
        # Trim old entries
        while self.total and self.total[0] < now - self.window_seconds:
            self.total.popleft()
        while self.failures and self.failures[0] < now - self.window_seconds:
            self.failures.popleft()
        self._update_state()

    def _update_state(self):
        if self.state == "open":
            if time.time() - self.opened_at > self.cooldown_seconds:
                self.state = "half-open"
            return
        if len(self.total) >= 10:  # Minimum sample size
            failure_rate = len(self.failures) / len(self.total)
            if failure_rate > self.failure_threshold:
                self.state = "open"
                self.opened_at = time.time()

    def allow_request(self):
        return self.state != "open"

That's the difference in one glance. The circuit breaker watches outcomes. Admission control watches state.


Why the Confusion Is Dangerous

Here's the scenario I keep seeing. A team deploys an LLM service. Traffic spikes. Latency climbs. GPU memory fills. Requests start timing out.

Their response? "We need a circuit breaker."

No. You need admission control. The circuit breaker would have tripped after the timeouts started. That's 30 seconds of garbage responses hitting your users. Admission control would have rejected excess requests before the GPU ran out of memory, keeping the admitted requests fast.

The inverse happens too. A team gets a dependency failure — their vector database goes down, so every RAG request fails after 10 seconds of waiting. They implement admission control to limit concurrency. But the requests aren't failing because of concurrency. They're failing because the retrieval step is broken. Admission control just makes the failure slower and more confusing.

The real fix was a circuit breaker on the retrieval client. But they spent two weeks tuning admission control parameters thinking they'd solve it.

I made this mistake myself. In 2024, we were building a real-time document analysis system for a legal tech client. Their peak load was predictable — court filings dropped at 5 PM EST. We had admission control dialed in perfectly. Then their PDF parsing service (third-party) went down for 45 minutes. Our LLM admission controller was still saying "all good, come on in." Every admitted request failed. We burned GPU quota on requests that were doomed from the start.

That's when I stopped seeing these as competing tools and started seeing them as complementary layers.


Admission Control vs Autoscaling for LLM Inference

This is another distinction people get wrong. And it matters because autoscaling is the wrong tool for most LLM overload problems.

Admission control for vLLM inference server handles the burst problem. Autoscaling handles the trend problem.

If your traffic grows steadily over 10 minutes, autoscaling should have anticipated it and added replicas. If your traffic spikes in 10 seconds — a viral post, a product launch, a deadline — autoscaling can't react fast enough. Kubernetes HPA has a default 15-second metrics interval plus a 30-second stabilization window. Your GPU is OOM by then.

Even fast autoscalers have cold-start problems. Spinning up a new inference replica means:

  1. Kubernetes schedules a pod — 2-5 seconds
  2. Container pulls the model weights — 30-90 seconds (unless cached)
  3. Model loads into GPU memory — 20-60 seconds for a 7B model on A100
  4. Warmup requests to trigger CUDA kernels — 5-15 seconds

You're looking at 60-170 seconds minimum to add capacity. A traffic spike destroys you in the first 20 seconds.

Admission control is instant. It protects the capacity you have right now while autoscaling brings up more.

Here's how we structure it at SIVARO:

Request → Rate Limiter → Admission Controller → Queue → vLLM Engine
    ↑                        ↑
Circuit Breaker ← Error/Failure Monitoring

The rate limiter handles per-user fairness. The admission controller protects aggregate system capacity. The circuit breaker watches for systemic failures. Autoscaling operates in the background, watching queue depth trends, not instantaneous load.


Practical Admission Control for vLLM

Let's get concrete. You're running vLLM in production. How do you actually implement admission control?

Option 1: vLLM Native Limits

vLLM has built-in admission control via --max-num-seqs. When you hit that limit, new requests wait in the engine's queue. That's actually a blocking admission control — it doesn't reject, it holds.

bash
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3.1-70B-Instruct \
    --tensor-parallel-size 4 \
    --max-num-seqs 128 \
    --max-num-batched-tokens 16384 \
    --queue-max-size 256

If you set a queue max size, requests beyond that get rejected with a 503. That's real admission control. But it's coarse — you're rejecting based on request count, not estimated compute cost.

Option 2: Token-Aware Admission

This is where it gets interesting. In 2025, we built a token-budget architect for a customer service automation platform. Instead of counting requests, we estimate the work each request will do.

python
def estimate_prompt_tokens(request):
    # Rough heuristic — prompt length in tokens
    return len(request["prompt"]) / 4  # ~4 chars per token

def estimate_completion_tokens(request):
    # Use max_tokens if specified, else fallback to model prediction
    if request.get("max_tokens"):
        return request["max_tokens"]
    # Simple model: industry average completion for similar tasks
    return estimate_from_task_type(request["task"])

def admit_request(request, current_utilization, max_utilization):
    estimated_tokens = estimate_prompt_tokens(request) + estimate_completion_tokens(request)
    projected_utilization = current_utilization + estimated_tokens
    if projected_utilization > max_utilization:
        return False, "Token budget exceeded"
    return True, estimated_tokens

This matters more than you think. A request with a 2-token response ("yes" or "no") costs 50x less than a request streaming 2,000 tokens of a legal document. Counting requests treats them identically. That's naive.

Option 3: Distributed Admission

If you have multiple vLLM replicas behind a load balancer, admission control needs to be centralized or at least coordinated. We use Redis to track aggregate GPU queue depth across replicas:

python
import redis

class DistributedAdmissionController:
    def __init__(self, redis_client, max_total_active=256):
        self.redis = redis_client
        self.max_total_active = max_total_active

    def try_admit(self, request_id, estimated_tokens):
        pipe = self.redis.pipeline()
        key = "llm:active_tokens"
        current = int(self.redis.get(key) or 0)
        if current + estimated_tokens > self.max_total_active:
            return False
        # Atomic increment with expiry
        pipe.incrby(key, estimated_tokens)
        pipe.expire(key, 120)  # Safety valve
        pipe.execute()
        return True

    def release(self, request_id, estimated_tokens):
        self.redis.decrby("llm:active_tokens", estimated_tokens)

This is the admission controller working before the request hits any single engine. It's the difference between one GPU overheating and the whole fleet degrading gracefully.


Where the Circuit Breaker Fits

Where the Circuit Breaker Fits

Now, layer the circuit breaker on top. Admission control said "yes, come in" because the system has capacity. But the model is hung on the previous request. Or the output parser is failing. Or the GPU is in a bad state.

The circuit breaker watches completion rates. If admitted requests are failing more than 50%, it opens. Even if there's capacity. Even if admission control says "we're fine."

python
class LLMRequestHandler:
    def __init__(self, admission_controller, circuit_breaker, engine):
        self.admission = admission_controller
        self.breaker = circuit_breaker
        self.engine = engine

    async def handle(self, request):
        # Layer 1: Circuit breaker check (fastest to evaluate)
        if not self.breaker.allow_request():
            return status.HTTP_503_SERVICE_UNAVAILABLE, "Circuit open — please retry in a moment"

        # Layer 2: Admission control check
        admitted, token_budget = self.admission.try_admit(request)
        if not admitted:
            return status.HTTP_429_TOO_MANY_REQUESTS, "System saturated — please back off"

        try:
            result = await self.engine.generate(request)
            self.breaker.record(success=True)
            return result
        except Exception as e:
            self.breaker.record(success=False)
            raise
        finally:
            self.admission.release(request, token_budget)

This is the architecture. Not either/or. Both layers, each with its own job.

The circuit breaker uses a different retry semantic than the admission controller. When admission control rejects you, you should back off with exponential jitter. When the circuit breaker is open, you should not retry — you should fall back to a different strategy entirely (cached responses, smaller model, or a clear error to the user).


The Failure Modes of Getting This Wrong

Let me walk through three real failure modes I've seen, each with the wrong tool applied.

Failure Mode 1: "Circuit Breaker Won't Save You From Admission Control Prevented 429s"

A financial document processing startup came to us in early 2025. They had a circuit breaker on their OpenAI API client. Good. But their own inference servers (vLLM on A100s) were returning 429s during market hours. The circuit breaker on the client saw 429s as "failures" and opened — then no requests went through at all, even when the server had recovered.

The fix: the circuit breaker should treat 429 as a server protection signal, not a failure. The client should implement proper backoff instead. Server-side admission control should be tuned to return 429 only when the system really can't handle more.

Failure Mode 2: "Admission Control Prevented the Model Degradation"

A chat application had admission control set too aggressively. 128 concurrent requests max. Their model could handle 256 with only slightly degraded latency. But because they capped at 128, they had to add more replicas than necessary, quadrupling GPU costs.

The lesson: admission control has a cost. Every request you reject could have been served with slightly higher latency. Dialing this in requires load testing with your actual workload, not theoretical token math.

Failure Mode 3: "Neither Tool Saves You From a Bad Model"

In the same chat application's case, the model would occasionally enter a degenerate state where it produced repetitive garbage tokens. Admission control happily admitted requests. The circuit breaker saw "successful" responses (they weren't errors technically) and stayed closed. Users got garbage at 50 tokens per second.

This required semantic circuit breaking — watching for output quality signals, not just request/response success. We built a lightweight classifier that scored response quality and fed into the circuit breaker:

python
def quality_score(response_text):
    # Simple heuristics — repetition, truncation, hallucination markers
    words = response_text.split()
    if len(words) < 5:
        return 0.3  # Too short to be useful
    unique_ratio = len(set(words)) / len(words)
    if unique_ratio < 0.3:
        return 0.1  # Degenerate repetition
    return 0.9  # Looks fine

Not perfect. But it catches the "model is broken but not erroring" case that neither admission control nor traditional circuit breakers handle.


How to Combine Them: An Operational Pattern

Here's the actual pattern we ship to clients:

Layer 0: Global rate limit (per-user, token-aware)

  • Prevents any single user from consuming the entire system

Layer 1: Admission control (system health)

  • Watches GPU queue depth, current tokens in flight, KV cache utilization
  • Rejects with 429 when projected load exceeds capacity
  • Rejection carries a Retry-After header

Layer 2: Circuit breaker (system behavior)

  • Watches success rate, error types, latency percentiles
  • Opens when failure rate exceeds threshold even if the system has capacity
  • Stays open for a cooldown period, then half-opens to test recovery

Layer 3: Autoscaling (trend management)

  • Watches admission control rejection rate as a signaling metric
  • If you're rejecting more than 5% of requests for 2 consecutive minutes, scale up

The key insight: admission control rejection rate is your best autoscaling signal. Not CPU. Not GPU utilization. Request rejection rate. It tells you exactly how much you're leaving on the table.

python
from kubernetes import client, config
from kubernetes.client.rest import ApiException

def autoscaled_replicas(rejection_rate_5m):
    # Base: your minimum replicas
    base = 2
    # Each 5% rejection rate above 5% = 1 additional replica
    excess_rejection = max(0, rejection_rate_5m - 5)  # percentage points
    additional = int(excess_rejection / 5)
    return min(base + additional, 10)  # Hard max of 10

I know you're thinking: "This is just the Kubernetes HPA with custom metrics." Yes. It is. But most teams don't wire the admission controller as their autoscaling signal. They use CPU or memory or requests per second. All of which lag behind the actual bottleneck signal.


The Metrics That Actually Matter

You don't need 40 dashboards. You need six numbers:

Metric What It Tells You
Admission rejection rate How much demand exceeds capacity
Circuit breaker open duration How long systemic failures are persisting
Time-to-first-token (p50, p95) Perceived latency of admitted requests
Token generation speed (tokens/sec) Model throughput health
Queue depth behind admission control How much burst you're absorbing
GPU KV cache utilization Resource exhaustion trajectory

When we onboard a new LLM service at SIVARO, we set up these six. Not because we're minimalists. Because every additional metric creates noise that obscures the decision.


What I'd Tell My 2024 Self

If I could go back to that legal tech project in 2024, I'd tell myself:

  1. Admission control is non-negotiable for production LLM services. You cannot autoscale fast enough.
  2. Circuit breakers are non-negotiable for dependencies. You cannot admission-control your way out of upstream failures.
  3. The circuit breaker belongs on the client of any dependency. The admission controller belongs in front of the model.
  4. Autoscaling uses admission control rejection rate as its signal, not CPU.

I thought this was a tooling problem. Turns out it was an architectural pattern problem. The tools are easy to set up. The pattern is what takes time to get right.

I also thought we could build one "universal" request handler that all our LLM clients could use. Turns out that's over-engineering. The admission control config depends on your GPU count, your model size, your traffic patterns. The core pattern is universal, but the tuning has to be workload-specific.

And no library will tell you the right max_num_seqs for your workload. That requires load testing. Run 50 concurrent requests. Measure latency. Run 100. Find the inflection point where p95 latency doubles for a 10% increase in concurrency. That's your admission limit.


FAQ: Admission Control vs Circuit Breaker for LLMs

Q: Can I use a circuit breaker as my primary overload protection?
Not effectively. A circuit breaker trips after failures occur. By then, you've already served bad responses or degraded the experience for admitted requests. Admission control prevents the overload in the first place.

Q: My admission controller returns 429 but clients retry rapidly and make things worse. What do I do?
Return a Retry-After header with a meaningful value. And make sure your clients respect it. We set Retry-After: 5 and implement exponential backoff with full jitter server-side (even though the backoff is a client responsibility).

Q: Is admission control the same as a queue?
No. A queue buffers excess load. Admission control drops or redirects it. Buffering can be useful for absorbing micro-bursts, but unbounded queues in front of LLM inference cause stale requests and memory pressure.

Q: How do I choose the circuit breaker failure threshold?
Start with your user tolerance. If 5% of requests fail, do users notice? Test with degradation. We typically start at 50% failure over a 60-second window — aggressive enough to trip before a full cascade, tolerant enough to handle single-request glitches.

Q: Should admission control reject or queue?
Reject, with a clear backoff contract. LLM inference is expensive — if you queue requests, they consume memory and the model state changes under them. A 429 with Retry-After is honest about the situation. Queuing hides the problem until it's too late.

Q: Does this matter for LLM APIs I don't host myself (OpenAI, Anthropic)?
Yes. You should have admission control on your side to ensure you're not exhausting your API quota with requests that will fail anyway. And you should definitely have a circuit breaker on the API client so a provider incident doesn't cascade through your service.

Q: What if my LLM is stateless and I can scale horizontally infinitely?
That's the dream. But GPU allocation is rarely truly elastic. Cloud providers have quota limits, and even in Kubernetes, GPU nodes take minutes to cold-start. Admission control is the buffer that keeps you alive during those minutes.

Q: How do I monitor both without going insane?
Start with the six metrics above. Set alerts on: rejection rate > 10% sustained, circuit breaker open > 5 minutes, time-to-first-token p95 > 3x your SLO. Ignore everything else initially.


A Final Point About Cost

A Final Point About Cost

Every good LLM system I've seen has a healthy obsession with GPU cost. Admission control directly affects your GPU bill because it determines how much work per GPU-second you squeeze out.

Over-admit, and you waste GPU cycles on failed or dropped requests. Under-admit, and you waste GPU cycles by running under-utilized. The sweet spot is a rejection rate of 1-5% during peak — that means you're occasionally pushing the system but not drowning it.

Circuit breakers don't save you money directly. But they prevent the cascading failures that cause you to over-allocate capacity to be safe. If you know your circuit breakers will catch systemic failures quickly, you don't need spare capacity for failure modes that get caught in seconds, not minutes.

At SIVARO, we run reservationless GPU pools for our own inference workloads — using elastic instances that can be reclaimed on 2 minutes' notice. That only works because admission control absorbs the transient unavailability when instances get reclaimed.


The admission control circuit breaker difference llm comes down to one framing: admission control says "no" to good requests when the system is busy. The circuit breaker says "no" to any requests when the system is broken.

You need both. You need them in that order. And you need autoscaling watching the admission controller's rejection rate to grow capacity before the rejection rate climbs further.

Build this layering before you need it. I promise you, the day your LLM service goes viral or your top customer's batch job hits at the wrong moment, you won't have time to architect it properly. You'll be debugging a cascading failure while your users tweet about how your AI product is down.

The pattern only takes a day to implement. An afternoon, really, if you use the code above as a starting point. That afternoon is the cheapest insurance you'll buy all year.


Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec.

Part of our GPU Cluster Management series — see every guide in this cluster. Fighting this in production? Explore AI Product Development.

Free · No Commitment · 48-Hour Delivery

Get a free infrastructure audit

2-hour remote session. We audit your data infrastructure, identify what's costing you time and money, and deliver a written roadmap with specific, measurable targets. No pitch.

Book Your Free Audit
N
Nishaant Dixit
Founder & Lead Engineer at SIVARO

Building data-intensive systems since 2018. 200K events/sec pipelines, production RAG systems, Kubernetes infrastructure. LinkedIn →

Start a Project
Need help with AI systems?

Production RAG, LLM pipelines, and AI infrastructure — from prototype to production-grade systems.

Explore AI Product Development