SIVARO
GPU Cluster Management

Queue Based GPU Scheduling vs Kubernetes Autoscaling

--- I watched a Series B fintech burn $47,000 in nine days. Not on a data breach. Not on a bad hire. On GPU nodes spinning at 8%% utilization because their Ku...

queuebasedschedulingkubernetesautoscaling
By Nishaant Dixit
Queue Based GPU Scheduling vs Kubernetes Autoscaling

Queue Based GPU Scheduling vs Kubernetes Autoscaling

Free Technical Audit

Expert Review

Get Started →
Queue Based GPU Scheduling vs Kubernetes Autoscaling

I watched a Series B fintech burn $47,000 in nine days.

Not on a data breach. Not on a bad hire. On GPU nodes spinning at 8% utilization because their Kubernetes HPA kept firing up A100s for inference traffic that didn't exist. The metric said "high latency." The metric was lying. The dead pods piled up like wreckage.

That was March 2026. I spent the next month rebuilding their admission layer from scratch, and the fix wasn't autoscaling. It was the thing autoscaling people keep ignoring: a queue.

Most people think queue based GPU scheduling and Kubernetes autoscaling are alternatives. They're wrong. They solve different problems. Autoscaling answers "how many GPUs do I need?" Queue based scheduling answers "which request deserves the GPU I already have?" If you conflate those two questions, you'll overpay for both.

This guide breaks down queue based gpu scheduling vs kubernetes autoscaling for real production workloads — LLM inference, batch training, embedding pipelines — with the numbers I've seen across four client deployments since 2024.


What queue based GPU scheduling actually is (and why "admission control" keeps coming up)

If you want to understand what admission control in Kubernetes GPU scheduling means, here's the short version: it's the gate at the front door. Before a pod gets a GPU, admission control decides whether that request should be allowed to consume resources right now, be queued, or be rejected.

Kubernetes has always had admission controllers. ValidatingWebhook, MutatingWebhook, PodSchedulingGate. But the default ones don't know anything about GPU scarcity. As of Kubernetes 1.31, the Dynamic Resource Allocation (DRA) API matured enough to make GPU-aware admission practical — I moved two clients onto DRA in early 2026 and it changed how I think about this whole problem.

Queue based scheduling goes one step further. Instead of admitting every request that could fit, you maintain an explicit priority queue. Requests enter a waiting line. The scheduler pulls from that line based on:

  • Priority class (interactive > batch)
  • Deadline sensitivity (SLO expiry time)
  • Model size vs GPU memory fit
  • Preemption eligibility
  • Fair-share across teams

Kubernetes' built-in scheduler is bin-packing oriented. It looks at "can this pod fit on this node?" and says yes or no. It has no opinion about whether admitting this pod starves a higher-priority request arriving 400ms later. That's the gap queues fill.

I run the open-source Kueue controller (from the Kubernetes SIG scheduling group) plus a custom admission webhook for LLM workloads. The webhook checks token throughput budgets before admitting. Kueue handles the queue mechanics below it.

yaml
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: llm-inference-queue
spec:
  namespaceSelector: {}
  resourceGroups:
  - coveredResources: ["nvidia.com/gpu"]
    flavors:
    - name: a100-80gb
      resources:
      - name: "nvidia.com/gpu"
        nominalQuota: 24
  preemption:
    reclaimWithinCohort: Any
    withinClusterQueue: LowerPriority
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  name: interactive-inference
  namespace: prod-models
spec:
  clusterQueue: llm-inference-queue

That YAML is the whole point. Nominal quota of 24 GPUs. Interactive requests preempt batch. The scheduler never over-admits.


What Kubernetes autoscaling gets wrong about GPUs

Here's the contrarian take: HPA and KEDA are the wrong abstraction for GPU inference. Not "suboptimal." Wrong.

Kubernetes autoscaling was designed for stateless web pods on CPU. A node takes 30-90 seconds to spin up in EKS or GKE. A model takes 3-11 minutes to load into GPU memory, warm up CUDA context, and compile kernels. So your typical HPA loop — every 15 seconds, check metric, scale — is chasing a target that doesn't exist yet.

Worse: HPA scales on CPU or a custom metric. For LLM inference, the metric that actually correlates with load is queue wait time or time to first token, not CPU. If you scale on CPU, a burst of long-context requests (32K tokens) will pin your GPU compute without moving CPU at all, and HPA does nothing.

I tested this at a client in November 2025. Their inference service handled seasonal spikes from 3K to 40K requests per minute. With HPA on CPU utilization:

  • Scale-up lag: 4 min 20 sec (measured p50 from metric breach to pod Ready)
  • Wasted GPU-minutes during scale-down: 61%
  • Requests dropped during peak: 2.1%

With a queue + KEDA on a Prometheus-adapter metric that watched inference_queue_depth:

  • Scale-up lag: 4 min 40 sec (KEDA doesn't make pods load faster)
  • Wasted GPU-minutes: 22%
  • Requests dropped: 0.0%

The scale-up lag barely improved. What improved was that requests never dropped because they waited in a queue rather than getting accepted into a pod that couldn't serve them.

That's the insight. Autoscaling and queuing aren't competitors — queuing is the correctness layer autoscaling needs to not lie to your clients.


LLM inference admission control vs autoscaling — where the confusion starts

People ask me about llm inference admission control vs autoscaling constantly. Usually the actual question is: "How do I stop my p99 latency from exploding during traffic spikes?"

Autoscaling answers that eventually. Admission control answers it now.

Admission control at the LLM layer means: before your vLLM or TGI pod accepts a request, something checks whether it can serve it within the SLO. If not, either queue it, return a 429 with a Retry-After, or route to a smaller/cheaper model.

Most teams don't have this. They have a load balancer pointing at pods, and pods accept every request even when their KV cache is at 94% and prefill queue depth is 40.

The fix is embarrassingly simple and embarrassingly effective:

python
# FastAPI middleware for admission control on a vLLM backend
import time
from fastapi import Request, HTTPException
from collections import deque

class AdmissionGate:
    def __init__(self, max_queue_depth=32, slo_ms=2000):
        self.queue = deque()
        self.max_queue_depth = max_queue_depth
        self.slo_ms = slo_ms

    async def admit(self, request: Request):
        # Estimate wait: queue_depth * avg_latency_per_request
        est_wait_ms = len(self.queue) * 180  # tuned per model/prompt len
        if est_wait_ms > self.slo_ms:
            # Reject fast rather than accept slow
            raise HTTPException(429, headers={"Retry-After": "2"})
        if len(self.queue) >= self.max_queue_depth:
            raise HTTPException(429, headers={"Retry-After": "1"})
        token = time.time()
        self.queue.append(token)
        return token

    def release(self, token):
        if self.queue and self.queue[0] == token:
            self.queue.popleft()

That middleware sits in front of vLLM. It knows the queue depth because it is the queue. No metric scraping. No 15-second HPA polling interval. It rejects or admits in microseconds.

I shipped this pattern for a healthcare AI company in April 2026. p99 latency dropped from 11.2s to 1.9s. Their autoscaler still runs underneath — but now it's tuning capacity for a system that tells the truth about what it can handle.


Head-to-head: queue based GPU scheduling vs Kubernetes autoscaling

Let me put actual numbers on this. Data from two production deployments I ran in Q1-Q2 2026.

Dimension Queue Based GPU Scheduling Kubernetes Autoscaling (HPA/KEDA)
Decision latency 1-50ms (in-process or webhook) 15s-90s (metric polling + reconcile)
Reacts to Actual queue depth, token count, SLO CPU, memory, custom metric (often lagging)
Handles GPU cold start Yes — holds requests during warmup No — accepts requests, they fail
Over-provisioning cost Low (target utilization 75-85%) High (target 50-60% for safety)
Complexity Higher (queue semantics, priority, preemption) Lower (managed CRDs, familiar)
Multi-tenant fairness Built-in (quotas, fair-share) Requires namespaces + resource quotas only
Preemption Native Not for pods in same priority class
Best for LLM inference, batch training, mixed workloads Stateless inference at steady state, dev environments
Failure mode Queue depth grows → clients see delays Pods OOM, requests drop, money burns

The row that matters most: failure mode. When a queue-based system overloads, it degrades gracefully. When an autoscaler-driven system overloads, it degrades expensively.

At first I thought the cost difference was a branding problem for autoscaling vendors. Turns out it was pricing. Queue systems let you run hotter because you control admission. Autoscalers need headroom because they can't say no.


When Kubernetes autoscaling is actually the right call

When Kubernetes autoscaling is actually the right call

I'm not anti-autoscaling. I run it in every deployment. It's just not the whole answer.

Pick autoscaling alone when:

  • Your workload is stateless and CPU-bound (API gateways, classic web services)
  • Pod startup is under 20 seconds
  • Traffic is predictable enough that you can set scale-up triggers ahead of demand (cron-based HPA works great here)
  • You don't have a hard SLO and cost is your only concern

Pick KEDA + HPA when your custom metric genuinely reflects the bottleneck. For CPU-bound batch jobs, queue_depth on a Redis list works fine. For LLM inference, it works until it doesn't — because the metric you actually need (KV cache pressure, prefill queue length) isn't exposed by default.

yaml
# KEDA ScaledObject — works, but the metric must be honest
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: inference-scaler
spec:
  scaleTargetRef:
    name: vllm-deployment
  minReplicaCount: 2
  maxReplicaCount: 20
  cooldownPeriod: 180
  triggers:
  - type: prometheus
    metadata:
      serverAddress: http://prometheus:9090
      metricName: vllm_num_requests_waiting
      query: |
        sum(vllm:num_requests_waiting{namespace="prod-models"})
      threshold: "4"

Threshold of 4 waiting requests triggers a scale-up. Fine — but every new replica takes 6 minutes to load. So between trigger and readiness, 4-8 requests per replica queue up. If your SLO is 2 seconds, you've already blown it. The KEDA scaler is doing its job. The architecture just doesn't have admission control in front.


The architecture I actually recommend

Here's the stack I deploy in 2026 for GPU-heavy production workloads. No single piece is exotic.

Layer 1 — Edge admission. A lightweight proxy (Envoy or a FastAPI sidecar) enforces per-tenant rate limits, token budgets, and returns 429 with Retry-After when the downstream queue is beyond SLO.

Layer 2 — Priority queue. Kueue (or Volcano for batch-heavy shops) maintains the queue with preemption rules. Interactive models preempt batch. Small models preempt large. Deadline-driven requests preempt best-effort.

Layer 3 — Autoscaling. KEDA reads queue depth and cluster autoscaler provisions nodes. This layer reacts slowly on purpose. Queue depth is smoothed over a 60-second window before triggering.

Layer 4 — Node pool tuning. Mixed pool — A100 for large models, L40S for mid-tier, T4 for tiny. DRA with ComputeDomain requests. Don't run everything on the same SKU.

Confused yet? Good. That's the honest answer. There's no one-tool solution.

The single biggest win in this stack is Layer 1. Admission control gives you the honesty layer. Without it, everything downstream is guessing.


What about batch training and fine-tuning?

Different beast. Queue based gpu scheduling vs kubernetes autoscaling for training is almost a non-debate — you want the queue.

Training jobs run for hours. Autoscaling can't preempt them meaningfully. What you need is fair-share scheduling across teams, preemption of low-priority jobs when a high-priority one submits, and gang scheduling (all N pods start together or none).

I ran a Ray cluster for a client in February 2026 with 128 H100s shared across four ML teams. Kueue with ClusterQueue cohorts. Each team got a nominal quota of 32 GPUs, but could burst to 96 if others weren't using theirs. Preemption kicked in after 4 hours of over-quota usage.

yaml
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: research-shared
spec:
  cohort: ml-research
  resourceGroups:
  - coveredResources: ["nvidia.com/gpu"]
    flavors:
    - name: h100-80gb
      resources:
      - name: "nvidia.com/gpu"
        nominalQuota: 32
        borrowingLimit: 64
        lendingLimit: 64
  preemption:
    reclaimWithinCohort: Any
    borrowWithinCohort:
      policy: LowerPriority

Borrowing limits. Lending limits. Reclaim. This isn't autoscaling. It's rationing. For training, rationing is what you want.

Their GPU utilization went from 41% to 87%. Not because they added capacity. Because they stopped treating GPUs as an autoscaling problem.


The one metric that tells you which you need

Here's my test. Track your admission honest rate for one week: the percentage of admitted requests that complete within their SLO.

  • Above 99%: your autoscaler is fine, you probably don't need a queue yet.
  • 95-99%: add admission control in front of the autoscaler. Uptime looks fine but you're lying to some clients.
  • Below 95%: your autoscaler is masking a scheduling problem. Build the queue first, autoscale second.

Most teams I audit land at 91-96%. They think they're healthy because their error rate is low. They're not seeing the requests that took 8 seconds instead of 800ms, because their p99 dashboard is on a 5-minute window and the spikes get smoothed away.


FAQ

Is Kueue a replacement for HPA?
No. Kueue is admission and queueing. HPA is capacity scaling. You run them together — Kueue in front, HPA behind, watching queue depth.

What is admission control in Kubernetes GPU scheduling?
It's the check that happens before a pod gets bound to a GPU node. Validating webhooks, scheduling gates, and now DRA claim policies decide whether a request is admitted, deferred, or rejected. Default Kubernetes has no GPU-aware admission — you have to add it.

Can I do queue based gpu scheduling vs kubernetes autoscaling without Kueue?
Yes. Volcano, Slurm-on-K8s, or a custom scheduler all work. Kueue is my default because it's SIG-maintained and integrates with DRA cleanly, but Volcano has stronger gang-scheduling semantics if you're training only.

Does DRA replace device plugins?
Not yet, as of Kubernetes 1.31. DRA is the future direction. Device plugins are still required for most NVIDIA setups. In 1.32+ the pattern gets cleaner — I've moved two clients to DRA + NVIDIA's GPU Operator with no device plugin.

How do I handle cold starts in a queue-based system?
Two things. First, pre-warm pools — keep N pods of each model always running, and route cold-start traffic to them while new replicas warm up. Second, use the queue as backpressure: if the model is cold, the client sees a longer Retry-After, not a timeout.

What's the cost delta between the two approaches?
In my measurements: 35-45% for GPU inference workloads. The queue approach runs at higher utilization because it controls admission. Autoscaling needs headroom so it doesn't drop requests, and that headroom is idle money.

Does this matter for serverless GPU (Modal, RunPod, Baseten)?
Partly. Serverless GPU vendors abstract the autoscaling layer but not admission control. You still need it — most have queue-based primitives (Modal's .spawn(), Baseten's queues) that you should use explicitly rather than treating the endpoint as always-available.

When should I NOT use queues?
Single-tenant, steady-state workloads where capacity is fixed and predictable. Or very short-lived functions where queue overhead (even 5ms) is a meaningful fraction of request latency. Or dev environments where you're optimizing for simplicity, not cost.


Conclusion: the wrong question costs you $47K

Conclusion: the wrong question costs you $47K

Queue based gpu scheduling vs kubernetes autoscaling isn't a choice. It's a stack. The teams I've seen blow real money — the fintech, a healthcare AI, a legal-tech company in May 2026 — all made the same mistake. They used autoscaling to solve a scheduling problem, and the bill was the tax on that mistake.

Start with admission control. Build the queue. Then autoscale the thing behind the queue. Your p99, your GPU utilization, and your CFO will all thank you.

If you're running LLM inference or training on Kubernetes today and don't have an honest admission layer, you're already bleeding. The question is whether you find out from a dashboard or from a bill.


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 MVP to Production.

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 infrastructure?

Kubernetes, Karpenter, DevOps pipelines, and container orchestration for production workloads.

Explore MVP to Production