Resource requests, limits and autoscaling

Declare what a container needs, understand QoS and eviction, then scale pods horizontally, vertically and the cluster itself.

Requests, limits and QoS

A request is what the scheduler reserves on a node and what the autoscaler measures against. A limit is the ceiling the kernel enforces at runtime. They are different numbers answering different questions, and leaving them out is the most common cause of instability.

spec:
  containers:
    - name: api
      image: ghcr.io/example/api:2.1.0
      resources:
        requests:
          cpu: 250m          # 0.25 of a core, reserved on the node
          memory: 256Mi      # used for scheduling and eviction ordering
        limits:
          cpu: "1"           # throttled above this, never killed
          memory: 512Mi      # exceeded means OOMKilled
QoS classConditionBehaviour under pressure
GuaranteedRequests equal limits for every container and resourceEvicted last; the safest for latency-sensitive work
BurstableRequests set, limits larger or absentEvicted after BestEffort; the normal choice for services
BestEffortNo requests and no limits anywhere in the podFirst to be evicted; only acceptable for scratch workloads
  • CPU is compressible: exceeding the limit means throttling, which shows up as latency, not crashes.
  • Memory is not compressible: exceeding the limit means the kernel kills the container, so OOMKilled is a limit-tuning problem first and a leak investigation second.
  • No memory request means the scheduler is guessing, and a node can be oversubscribed until something gets evicted.
  • A CPU request too high wastes capacity; too low makes the pod starve under contention. Measure with kubectl top and set requests near the steady-state usage.
kubectl top pods -n storefront
kubectl top nodes
kubectl get pod api-abc -o jsonpath='{.status.qosClass}'
kubectl describe pod api-abc | grep -A3 Limits

Horizontal Pod Autoscaler

The HPA changes the replica count of a Deployment or StatefulSet based on observed metrics. It depends on metrics-server for CPU and memory, and on an adapter for anything custom.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  minReplicas: 3
  maxReplicas: 30
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70      # percent of the REQUEST, not of the node
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: "200"
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300   # do not flap down immediately
      policies:
        - type: Percent
          value: 50
          periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 30
  • Targets expressed as utilisation are a percentage of the container request, so an HPA without requests silently has no metric to compare against.
  • The HPA scales up quickly and down slowly on purpose. Capacity that arrives late is an outage; capacity that lingers is a bill.
  • Autoscaling cannot fix a slow start-up. Pair it with a readiness probe and a realistic minReplicas, or new pods will arrive after the traffic spike has passed.
  • Scaling a StatefulSet scales state too. Make sure the application tolerates replicas appearing and disappearing.

Vertical scaling, cluster scaling and disruption

AutoscalerWhat it changesNotes
HPAReplica countNeeds metrics and requests; the default answer for stateless services
VPAContainer requests and limitsRecommends or applies sizes; restarts pods unless in recommendation-only mode
Cluster Autoscaler / KarpenterNumber and size of nodesAdds nodes for Pending pods, removes underused ones
PodDisruptionBudgetHow many pods may be downConstrains drains and node termination, not failures
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api
spec:
  minAvailable: 2                  # or maxUnavailable: 1
  selector:
    matchLabels:
      app: api
  • Do not run HPA and VPA on the same metric for the same workload: one changes replicas, the other changes requests, and they fight.
  • A PDB with minAvailable equal to the replica count blocks every drain for ever. Leave at least one pod of slack.
  • The Cluster Autoscaler only adds nodes when a pod is genuinely unschedulable, which is why a bad request size is also a scaling problem.
  • Set a memory limit on everything, but be careful with CPU limits: many teams run without CPU limits and rely on requests plus fair sharing to avoid throttling.
⚠️
An HPA makes a small problem worse as often as it helps. If each new replica opens more database connections and the database is the bottleneck, scaling out will take the database down faster. Scale the bottleneck, not the thing in front of it.

FAQ

My HPA shows unknown metrics. Why?
Either metrics-server is not running, or the target pods have no resource requests, so there is nothing to compute a percentage against. Check kubectl describe hpa for the exact condition.
Should I always set a CPU limit?
No. A CPU limit causes throttling even when the node has spare capacity, and throttling often shows up as mysterious tail latency. Requests guarantee your share; limits cap the burst. Decide per workload and measure.

Scheduling: affinity, taints and tolerations Storage: volumes, PersistentVolumes and StorageClasses

Last refreshed 2026-09-18.