Kubernetes QoS Class Calculator

Paste your workloads and find out which QoS class the kubelet will give each pod, which container decided it, and where that puts the pod in the eviction queue when a node runs short of memory. One container missing one limit demotes the whole pod, and nothing in kubectl tells you.

Paste below, or drop a file anywhere on this panel

Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.

The answer appears here

Paste on the left and press Classify. Nothing leaves this tab.

Examples

Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.

BestEffort

No requests or limits anywhere, so this pod is evicted first under pressure

apiVersion: v1
kind: Pod
metadata:
  name: debug
spec:
  containers:
    - name: shell
      image: busybox:1.36

Burstable

A limit above the request, and what that means for eviction order

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  template:
    spec:
      containers:
        - name: app
          image: api:1
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              memory: 256Mi

Common mistakes

These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

  1. Leaving resources unset on a production pod

    That is BestEffort, which is the first thing evicted under node pressure and gets the lowest CPU share under contention.

    Instead:Set requests at minimum. Requests alone give Burstable, which is a large improvement.

  2. Aiming for Guaranteed everywhere

    It requires limits equal to requests for every container including init containers, which wastes capacity for workloads that burst.

    Instead:Reserve Guaranteed for latency-sensitive workloads. Burstable is correct for most things.

  3. Forgetting that QoS is computed, not declared

    There is no qosClass field to set. It is derived from every container's requests and limits, so one sidecar without them changes the whole pod's class.

    Instead:Set resources on every container, sidecars included.

What it works out

All of it comes from the manifest, which is the point: the class is only visible on a running pod, and by then the workload is already in the eviction queue you did not intend.

The sidecar that costs you Guaranteed

QoS is a property of the pod, not of a container. One container short of one limit demotes every container in the pod to Burstable, and the usual culprit is a logging agent or a mesh proxy that arrived from a webhook with no resources on it. The report names the container that did it and says what the rest of the pod would have been without it.

Init containers and native sidecars count

Both are included in the computation, and a native sidecar is an init container with restartPolicy: Always, so it counts twice over: for the class and for the pod's memory request while it runs. An unresourced init container demotes a pod whose app containers are all correct.

Where each pod sits in the eviction order

The class is not the sort key. Under node memory pressure the kubelet asks first whether a pod is using more memory than it requested, so a Burstable pod sitting under its request is safer than one over it, and a Burstable pod with no memory request at all is in the same group as every BestEffort pod. Each workload is placed in that order and the diagram draws the queue.

The values that count as unset

Only cpu and memory take part, and only above zero. A container whose sole resource entry is nvidia.com/gpu or ephemeral-storage is BestEffort, and a literal cpu: 0 counts for nothing. Requests you did not write are also filled in: the API server copies each limit into the matching request, which is why a limits-only container is Guaranteed rather than Burstable.

Quantities compared as numbers

The kubelet compares values, not strings. cpu: 1 and cpu: 1000m are equal and that pod is Guaranteed. 1Gi and 1G differ by 7.4% and that pod is Burstable. Both are parsed exactly, through the same code as the quantity converter on this site, so nothing here turns on a rounded double.

oom_score_adj, and what it is not

Eviction is the kubelet reclaiming memory in advance. A node-level OOM is the kernel choosing under pressure it did not see coming, and it chooses by oom_score_adj: -997 for Guaranteed, 1000 for BestEffort, and a value scaled by the container's own memory request in between. A container hitting its own memory limit is a third thing again, and the class has no say in it.

What a QoS class is, and what it decides

Nobody writes a QoS class. The kubelet derives it from the requests and limits on every container in the pod, writes the answer to status.qosClass, and never changes it. It controls two things: which pods the kubelet evicts first when a node runs short of memory, and what oom_score_adj the container processes are given so the kernel knows who to kill. It controls nothing else, and in particular it has no effect on scheduling.

The three classes, stated exactly

Guaranteed needs every container in the pod, init containers included, to have both a CPU and a memory limit, with the request equal to the limit for both. BestEffort is the opposite end: no container sets any CPU or memory request or limit above zero. Burstable is everything in the middle, which is where most pods actually live. There is no partial credit and no per-container class.

Guaranteed   every container: cpu and memory limits set,
                              request == limit for both

BestEffort   no container sets any cpu or memory
             request or limit above zero

Burstable    anything else

One container decides for all of them

This is the case worth checking a manifest for. The app container is sized carefully, a sidecar is added later with no resources, and the pod that was meant to be Guaranteed is Burstable. Nothing warns about it: kubectl get pod does not print the class, the app container's own block still reads as correct, and the difference only appears as the wrong pod dying under memory pressure.

containers:
  - name: app              cpu 500m/500m, memory 512Mi/512Mi
  - name: istio-proxy      no resources at all

pod QoS: Burstable

the app container is unchanged and the whole pod
moved class, because the class belongs to the pod

Requests you did not write are still there

When a request key is absent and the matching limit is set, the API server copies the limit into the request. It happens per key, so a container with a CPU request and both limits gets its memory request filled in from its memory limit. The consequence surprises people twice over: a limits-only container is Guaranteed, and it reserves its full limit on the node even though no request appears anywhere in the file.

written:            stored:
  limits:             requests:
    cpu: 1              cpu: 1
    memory: 1Gi         memory: 1Gi
                      limits:
                        cpu: 1
                        memory: 1Gi

class: Guaranteed

Eviction order is about the request, not the class

When a node crosses a memory eviction threshold the kubelet sorts the pods on it. The first key is whether the pod is using more memory than it requested, then pod priority, then how far over the request it is. BestEffort pods come first because they have no request to be under, which also means a Burstable pod that never set a memory request is exactly as exposed. A Burstable pod comfortably under its request is not in that group at all, and sits with the Guaranteed pods in the second one.

evicted first
  |  pods over their memory request,
  |    lowest priority first,
  |    then furthest over first
  |
  |  everything else, only if the node is
  V    still over the threshold

a Burstable pod whose memory limit equals its
request cannot exceed it, so it never enters
the first group at all

Three different ways a container dies

They are constantly confused and only one of them involves the QoS class. A container that hits its own memory limit is killed by the kernel against its cgroup, immediately, and no other pod is affected: the class is irrelevant. A node under memory pressure is handled by the kubelet, which evicts whole pods in the order above. A node that runs out faster than the kubelet reacts is handled by the kernel OOM killer, which picks a process by oom_score_adj, and that is where the class is the input.

container over its own memory limit
  -> that container, OOMKilled, always

node under the eviction threshold
  -> kubelet evicts pods, by request-versus-usage

node out of memory, no warning
  -> kernel picks by oom_score_adj:
       Guaranteed   -997
       Burstable    1000 - 1000*memRequest/capacity
                    clamped to at least 1000 + (-997)
       BestEffort   1000

CPU throttles, memory kills

CPU is a compressible resource. A container over its CPU limit is throttled by the CFS scheduler for the rest of the 100ms period and continues, slower. Memory is not compressible: there is nothing to take back, so the only enforcement available is killing. That asymmetry is why a memory limit equal to the request is the safe setting and a tight CPU limit is a latency problem rather than a safety one.

cpu over limit      throttled, per 100ms period
memory over limit   OOMKilled, immediately

What Guaranteed actually buys, and what it does not

The one real performance reason to want it: on a node started with --cpu-manager-policy=static, a Guaranteed pod whose CPU limits are whole numbers of cores gets exclusive cores, and nothing else is scheduled onto them. A fractional CPU limit anywhere in the pod disqualifies it. What Guaranteed does not do is help you get scheduled. The scheduler only ever looks at requests, so two pods with identical requests are placed identically whatever their classes are.

Guaranteed + integer cpu limits + static policy
  -> exclusive cores, no other container on them

Guaranteed + cpu: 500m
  -> shared pool, same as everything else

scheduling: requests only. QoS is not an input.

It is decided once

The class is computed at admission and written to status.qosClass. There is no field to patch, no annotation, and in-place resource resize refuses any change that would move the pod between classes. Changing the class means a new pod, which means editing the template and rolling the workload.

kubectl get pod api-7d4f -o jsonpath='{.status.qosClass}'
Burstable

kubectl patch ... status.qosClass   no such thing

More kubernetes tools

Kubernetes YAML Generator Answer a few questions, get a manifest Kubernetes YAML Validator Is this YAML valid, and where is it wrong? Kubernetes Rollout & Probe Timing Restart loops, capacity dips and stuck drains Kubernetes Service Checker Will this Service have any endpoints? Kubernetes Network Policy Viewer What can reach what, and what just broke Kubernetes Manifest Diff What actually changed Kubernetes Resource Calculator What these manifests actually reserve Kubernetes RBAC Viewer Who can actually do what Kubernetes Deployment Generator See the capacity dip before you deploy Kubernetes Service Generator port, targetPort, and which one you meant Kubernetes StatefulSet Generator Stable names, and volumes that outlive you Kubernetes Node Capacity Planner Allocatable is not capacity Kubernetes Max Pods per Node Calculator The ENI limit, not the 110 default Kubernetes Label Selector Tester Which key failed, not just that one did Kubernetes Taint & Toleration Tester Tolerations permit, they do not attract Kubernetes Resource Name Validator RFC 1123 subdomain, label, or 1035 Kubernetes Label & Annotation Validator Why your image tag is not a legal label Kubernetes CrashLoopBackOff Guide The status is a timer, not a cause Kubernetes TLS Certificate Decoder When it expires, and what it covers Kubernetes ServiceAccount Token Decoder Bound or legacy, and when it dies Kubernetes imagePullSecret Generator The registry URL nobody gets right Kubernetes cert-manager Certificates Issuer scope, and the rate limit that costs a week Kubernetes Flux HelmRelease Generator With the source it cannot work without Kubernetes Helm Chart Generator Chart.yaml, values, and working templates Kubernetes Argo CD Application Generator The defaults that are off, turned on Kubernetes Ingress Generator pathType, and the TLS secret that fails quietly Kubernetes HPA Generator Why it says unknown and never scales Kubernetes SecurityContext Generator Pod level, container level, and which wins Kubernetes Probe Generator How long it gets to start before liveness kills it Kubernetes PVC Generator Access modes your storage can actually do Kubernetes ResourceQuota Generator And the LimitRange that stops it breaking deploys Kubernetes CronJob Generator The fields that decide whether it runs Kubernetes NetworkPolicy Generator With the DNS rule already in it Kubernetes RBAC Generator And what it actually grants Kubernetes PodDisruptionBudget Generator Can this budget ever be satisfied? Kubernetes API Deprecation Checker What breaks on the next upgrade Kubernetes Kubeconfig Viewer Read it without handing it to anyone Kubernetes Quantity Parser What 512Mi, 100m and 1e3 actually mean Kubernetes Multi-Document YAML Splitter One file per object, in apply order Kubernetes .env to ConfigMap & Secret Split the credentials out on the way Kubernetes Secret Decoder See what is actually in there Kubernetes Ingress to Gateway API Which annotations silently stop working Kubernetes Manifest Visualizer What points at what, and what points at nothing Kubernetes Patch Tester Which --type, and what it deletes Kubernetes kubectl JSONPath Tester kubectl's dialect, not generic JSONPath Kubernetes Node Affinity Tester Required excludes, preferred only scores Kubernetes Helm Values Diff A missing key is a third value Kubernetes Pod and Service CIDR Fixed at cluster creation Kubernetes Secret Encoder base64 is not encryption Kubernetes API Version Checker Will this apply on that cluster Kubernetes Go Template Tester Go truthiness is not JavaScript's Kubernetes Cluster Cost Estimator At your rates, not a price table Kubernetes Kustomize Build Transformers run in kustomize's order Kubernetes Helm Chart Validator version is SemVer, appVersion is not

Elsewhere on the site