Quality of Service (QoS) Classes in Kubernetes
A complete, plain-English guide to Guaranteed, Burstable and BestEffort pods — how Kubernetes assigns them, how the Linux kernel enforces them, and how they decide who survives when a node runs out of room.
Imagine a lifeboat that is supposed to hold twenty people, but the ship’s captain, trying to be generous, has quietly promised seats to thirty. On a calm day nobody notices. When the storm hits and the boat gets heavy, someone has to decide who stays aboard and who gets asked to leave. If that decision is made in a panic, it will be unfair, random and sometimes disastrous. If it was written down calmly before the voyage, everyone knows what to expect.
A Kubernetes node is that lifeboat. It has a fixed amount of CPU and memory, yet the cluster routinely promises more than it has, because most programs do not use all they ask for at the same time. Quality of Service classes are the calm, written-down plan for what happens when the promises cannot all be kept. In this guide you will learn what the three classes are, how Kubernetes picks one for each pod without being asked, how the Linux kernel turns that choice into real behaviour, and how to use the classes to keep your important workloads safe and your cluster full but stable.
1Why Quality of Service Exists
Sharing a machine is cheap and efficient, right up to the moment everyone wants the same thing at once.
The problem of sharing
A Kubernetes cluster is a pool of machines called nodes, and each node runs many pods side by side. Running many programs on one machine is the whole point, because it saves money and keeps hardware busy. But a machine has only so much CPU time and so much memory. When the programs on it collectively ask for more than exists, something must give.
Two very different things can happen. If the shortage is about CPU, programs simply run more slowly because they take turns on the processor. If the shortage is about memory, there is no polite way to share. Memory is either available or it is not, and when it runs out, the machine must stop some program to free it. Without a plan, the system would choose a victim by chance, and the unlucky one might be your payment service while a harmless test job survives.
Think of an airline that sells a few more tickets than seats, because some passengers never show up. Usually this works. On a rare full flight, the airline already has rules: people with premium tickets and confirmed seats are protected, people on standby are asked to wait for the next plane. Kubernetes QoS classes are the airline’s boarding rules for your pods.
What QoS actually is
A Quality of Service class is a label that Kubernetes attaches to every pod, summarising how firmly that pod has reserved the resources it uses. There are exactly three labels: Guaranteed, Burstable and BestEffort. The label is not decorative. The kubelet, which is the agent running on each node, and the Linux kernel underneath both consult it when deciding how to arrange control groups, which process to stop first when memory runs out, and which pods to remove when the node is under pressure.
The most surprising thing for newcomers is that you never write the class yourself. There is no field called “qosClass” that you set. Instead, Kubernetes works it out from the way you described your pod’s resource needs. Change those descriptions, and the class changes with them. This design is clever, because it ties the safety level directly to the promises you were willing to make.
“QoS classes make my pod faster.” They do not. They do not speed up your code or give it priority on the network. They decide how your pod is treated when resources are short, and how firmly capacity is reserved for it. A Guaranteed pod is protected, not turbocharged.
A story of a bad night
Scenario: the batch job that ate the memory
A data team deploys a report-generating job with no resource settings at all. One night the job reads a file ten times larger than usual and balloons in memory. On the same node sits a checkout service whose owners did set careful requests and limits. Because of QoS classes, the kernel and the kubelet go after the report job first, and the checkout service keeps running. Without the classes, the outcome would depend on luck.
2Resource Basics: Requests, Limits and Two Kinds of Resource
You cannot understand QoS without first understanding the two numbers every container can carry.
Each container in a pod may declare how much CPU and memory it needs. Kubernetes gives you two knobs for each resource. The request is the amount you are asking Kubernetes to set aside for you, and it is what the scheduler uses to decide where a pod fits. The limit is the ceiling that the container is not allowed to exceed. Between those two numbers lies a range where the container can use extra if it is available.
Request
The minimum amount reserved for the container. The scheduler counts it when picking a node, and the kernel uses it to share CPU fairly under contention.
Limit
The maximum amount the container may use. Going past a CPU limit means being slowed down; going past a memory limit means being stopped.
Allocatable
The part of a node’s capacity that pods may use after Kubernetes sets aside room for the operating system, its own agents and eviction safety margins.
Overcommit
Promising more in total limits than the node really has. It is safe only because programs rarely peak at the same moment.
Compressible resource
A resource that can be squeezed without killing anything. CPU is the main example: less of it just means slower work.
Incompressible resource
A resource that cannot be squeezed without taking it back by force. Memory is the main example: once given, it can only be reclaimed by stopping something.
CPU is like a busy road. When too many cars arrive, everyone slows down but everyone still gets through eventually. Memory is like seats in a theatre. When every seat is taken, the next person cannot squeeze in; someone has to leave before anyone new can sit down.
How CPU and memory behave differently
| Question | CPU | Memory |
|---|---|---|
| Can it be shared smoothly? | Yes, by time-slicing | No, it is held until released |
| What happens at the limit? | The container is throttled and runs slower | The container is killed by the kernel |
| What happens at node shortage? | Pods get CPU in proportion to their requests | The kernel or kubelet stops some pods |
| Typical unit | Cores or thousandths of a core, called millicores | Bytes, usually written in mebibytes or gibibytes |
| Role in QoS | Shapes fairness and exclusive core options | Shapes who is killed first and when |
The node’s real budget
A node with sixteen gigabytes of memory does not offer all sixteen to pods. The operating system needs some, the kubelet and container runtime need some, and Kubernetes keeps a safety buffer so that it can react before memory truly runs out. What remains is the allocatable amount, and that is the budget the scheduler works against. Understanding this budget explains why a node that looks half empty can still refuse a new pod.
A request of “500m” CPU means half a core, not five hundred cores. For memory, “Mi” and “Gi” mean powers of two, while “M” and “G” mean powers of ten, so the two can differ by several percent. Mixing them up is a classic source of confusing behaviour around limits.
3The Three QoS Classes
Guaranteed, Burstable and BestEffort: three promises of increasing flexibility and decreasing protection.
Guaranteed
Every container has both CPU and memory limits and requests, and each request equals its limit. The pod has reserved exactly what it may use, so it is the most protected.
Burstable
At least one container has some request or limit, but the pod does not meet the Guaranteed rules. It has a reserved floor and may stretch above it when room exists.
BestEffort
No container has any CPU or memory request or limit. The pod reserved nothing, so it is the first to lose out when resources are short.
flowchart TD
A["Look at every container in the pod"] --> B{"Does any container have any CPU or memory request or limit?"}
B -->|"No"| C["BestEffort"]
B -->|"Yes"| D{"Does every container have CPU and memory limits, with requests equal to limits?"}
D -->|"Yes"| E["Guaranteed"]
D -->|"No"| F["Burstable"]
The diagram shows the order of the questions. First Kubernetes asks whether the pod mentioned resources at all. If it said nothing anywhere, the pod is BestEffort. If it said something, the next question is whether it said everything and said it strictly: all containers, both resources, request equal to limit. A perfect answer gives Guaranteed. Every other partial answer lands in the middle as Burstable.
Guaranteed is a reserved seat with your name on it, paid in full. Burstable is a ticket with a reserved seat plus permission to move into an empty first-class seat if one is free. BestEffort is a standby ticket: you board only if there is room and you are the first to be asked to step off when the plane fills.
A side-by-side view
| Aspect | Guaranteed | Burstable | BestEffort |
|---|---|---|---|
| Resource settings | Requests equal limits for CPU and memory in every container | Some settings, but not the full Guaranteed pattern | None at all |
| Reserved capacity | Exactly what it can use | Its requests only | Nothing |
| Can use more than its reservation? | No, its ceiling equals its floor | Yes, up to its limits if present | Yes, whatever is free |
| Kernel memory-kill preference | Last | In the middle | First |
| Node-pressure eviction tendency | Least likely to be chosen | Depends on how far above its requests it is | Most likely to be chosen |
| Typical use | Databases, core services, latency-sensitive systems | Most web services and APIs | Disposable or experimental work |
Why not make everything Guaranteed?
It is tempting to ask for the strongest class everywhere. The catch is cost. A Guaranteed pod reserves its full ceiling at all times, even when it is idle, and the scheduler cannot hand that capacity to anyone else. Run enough Guaranteed pods and your nodes look full on paper while the machines sit mostly idle. Burstable and BestEffort exist because sharing unused capacity is where most of the savings in a cluster come from.
Protection and efficiency pull in opposite directions. The art of QoS is to pay for protection only where an outage would really hurt, and to let everything else share capacity freely.
4How Kubernetes Assigns the Class, Step by Step
The rules look simple, but small details quietly change the outcome.
Every container counts, including the helpers
The class belongs to the whole pod, not to a single container, so Kubernetes inspects every container. That includes init containers, which run first to prepare the pod, and the long-running helper containers often called sidecars. For a pod to be Guaranteed, each of them must have both CPU and memory limits, with the requests matching. One forgotten helper is enough to push the whole pod down to Burstable.
The pod is submitted
You apply the pod, or a controller such as a deployment creates it. Your resource settings are part of the description.
Defaults and policies are applied
Admission steps may fill in missing values. A namespace rule called a LimitRange can add default requests and limits, and webhooks may inject extra containers.
If only a limit is given, the request follows
When a container sets a limit but no request, Kubernetes copies the limit into the request. This helps many pods become Guaranteed with less typing.
The class is calculated and stored
Kubernetes applies the decision flow and saves the result in the pod’s status. You can read it back at any time.
The class is locked for the pod’s life
The class cannot be edited on a running pod. Even features that resize resources in place are not allowed to change it. To get a different class you replace the pod.
Think of a school trip where the whole class gets one colour badge. The badge is decided by the least-prepared student: if just one child forgot a signed permission slip, the whole group gets the lower-status badge, no matter how well the others prepared.
Worked examples in words
Example A: perfect match, Guaranteed
A pod has one container with a CPU request and limit of one core, and a memory request and limit of two gibibytes. Every rule is met, so the pod is Guaranteed.
Example B: only memory set, Burstable
A pod has memory request and limit set but nothing for CPU. Because CPU is missing, the Guaranteed rule fails, yet something was specified, so the pod is Burstable.
Example C: requests lower than limits, Burstable
A container requests half a core and one gibibyte, but its limits are two cores and four gibibytes. It has room to grow beyond what it reserved, which is exactly what Burstable means.
Example D: nothing set, BestEffort
A quick test pod has no resource settings on any container. It is BestEffort and will be the first to be stopped if the node struggles.
Example E: an extra helper spoils Guaranteed
An application container is perfectly configured, but a logging helper container added by a platform tool has no limits. The pod drops from Guaranteed to Burstable without anyone changing the application.
Service meshes, security agents and logging tools often add containers to your pods automatically. If those injected containers lack matching requests and limits, your carefully built Guaranteed pod quietly becomes Burstable. Always check the final class on a running pod, not just the manifest you wrote.
A newer option: pod-level resources
Recent Kubernetes versions have been adding the ability to describe resources for the pod as a whole, not only per container. This can simplify pods with many small helpers, because a single pod-level budget can be set. The feature is newer and still maturing across versions, so check your cluster’s release notes to see whether it is available and how it influences the class on your version before relying on it.
5Under the Hood: Control Groups and the Linux Kernel
Kubernetes does not enforce anything by itself. It writes your intentions into Linux control groups, and the kernel does the rest.
What is a control group?
A control group, usually shortened to cgroup, is a Linux feature for putting processes into a bucket and attaching rules to the bucket: how much CPU time it gets, how much memory it may hold, how many processes it may create. Buckets can sit inside other buckets, forming a tree, and rules at the top apply to everything below. Containers are, at the bottom, just processes placed in the right cgroup buckets.
Imagine a company budget. The company has a total, each department gets a share, and each team inside a department gets a smaller share. The finance system checks spending against the nearest budget bucket. Cgroups are those budget buckets for CPU and memory, and Kubernetes is the manager who creates the buckets and fills in the numbers.
The QoS-shaped tree
The kubelet builds a family of cgroups that mirror the QoS classes. All pods live under one top bucket for Kubernetes workloads. Inside it, Burstable pods and BestEffort pods each have their own sub-bucket, while Guaranteed pods sit directly under the top bucket, side by side with those sub-buckets. Each pod then has its own bucket, and each container its own below that.
flowchart TD
A["All Kubernetes pods bucket"] --> B["Guaranteed pod buckets"]
A --> C["Burstable parent bucket"]
A --> D["BestEffort parent bucket"]
C --> E["Burstable pod buckets"]
D --> F["BestEffort pod buckets"]
B --> G["Container buckets"]
E --> G
F --> G
This shape has a purpose. Putting BestEffort pods in a shared sub-bucket with a very small CPU weight means that, when the node is busy, all of them together can only take a tiny slice, while Guaranteed and Burstable pods keep their reserved shares. The tree makes the class a real property of the machine, not just a label.
How requests and limits become kernel settings
| You write | The kernel receives | What it means in practice |
|---|---|---|
| CPU request | A CPU weight, called shares in older systems | Under contention, the container gets CPU time in proportion to its weight; when the node is idle, the weight does nothing |
| CPU limit | A bandwidth quota per scheduling period | The container may use only so much CPU time every short period, and is paused until the next period once it runs out |
| Memory limit | A hard maximum on the memory bucket | Going over triggers the kernel’s out-of-memory killer inside that bucket |
| Memory request | Mostly used by the scheduler and the OOM score | On newer setups, an optional feature can also protect this amount from being reclaimed |
Linux has an older and a newer version of cgroups, usually called version one and version two. The ideas are the same, but file names and some capabilities differ. Newer Kubernetes releases favour version two, which enables features such as better memory protection. Check which version your nodes use when reading low-level documentation.
The surprising behaviour of CPU limits
A CPU limit does not mean “you may use at most this fraction of the machine at any moment.” It means a budget of CPU time per tiny period, commonly a tenth of a second. A container with a limit of one core that runs on four threads at once can burn through its whole period budget in a quarter of the period and then be paused for the rest. The result can be sudden pauses, called throttling, even when the node as a whole is nearly idle. This is why CPU limits are a hot topic in latency-sensitive systems, and we will return to it later.
6Memory Pressure and the Out-of-Memory Killer
When memory runs out, the Linux kernel plays judge. QoS classes quietly influence its verdict.
Two ways a container dies from memory
People often use “OOMKilled” and “evicted” as if they meant the same thing. They do not, and the difference is essential for debugging. A container is OOMKilled when the kernel’s out-of-memory killer ends its process, either because the container hit its own memory limit or because the whole node ran out of memory. A pod is evicted when the kubelet, acting earlier and more politely, decides to remove it to relieve pressure on the node.
| Aspect | OOM kill | Eviction |
|---|---|---|
| Who acts? | The Linux kernel | The kubelet |
| When? | Immediately when memory cannot be found | When a configured threshold is crossed, often before real exhaustion |
| What is stopped? | One or more processes, chosen by an OOM score | Whole pods, chosen by a kubelet ranking |
| What do you see? | Container status reason “OOMKilled” and exit code 137 | Pod status “Evicted” with a message about the resource under pressure |
| Does the pod restart? | The container restarts according to the pod’s restart policy | The pod is gone; a controller must create a replacement, usually elsewhere |
How the kernel picks its victim
Every Linux process has an out-of-memory score. When memory is exhausted, the kernel tends to stop processes with the highest score first. Kubernetes adjusts these scores using a setting called the OOM score adjustment, and it chooses the number according to the pod’s QoS class. This is how the class becomes a direct instruction to the kernel.
Strongly protected
Receives a very low adjustment, around minus 997, which makes the kernel reluctant to choose it. Only essential system processes are protected more.
Scaled by its request
Receives a number between about 2 and 999 that gets lower as the pod’s memory request grows as a share of the machine’s memory. Bigger reservations mean better protection.
First in line
Receives the highest adjustment, 1000, making its processes the most attractive targets for the kernel.
Node agents
The kubelet and container runtime get even lower values, so the machine’s own management tools survive when workloads misbehave.
Picture a lifeguard who must clear a crowded pool by whistle. Each swimmer wears a coloured wristband showing how much they paid for their spot. When the whistle blows, the lifeguard asks the swimmers with the cheapest wristbands to leave first. QoS-based scores are those wristbands, and the kernel is the lifeguard who reads them in a hurry.
Reading the Burstable formula without fear
The Burstable calculation sounds technical but behaves intuitively. Start from the maximum score of one thousand and subtract a portion proportional to how much memory the pod requested compared with the whole machine. A tiny request subtracts almost nothing and leaves the pod near the top, close to BestEffort. A large request subtracts a lot and moves the pod toward the protected end. The result is clamped so that Burstable pods always stay above the Guaranteed range and below the BestEffort range.
The practical message is simple: for Burstable pods, setting an honest memory request is not only a scheduling hint, it is also a protection setting. A pod that requests too little memory looks cheap to the kernel and is stopped sooner, even if it is a very important service.
Guaranteed pods are protected from being chosen during node-wide shortages, but they still cannot exceed their own memory limit. A Guaranteed pod that leaks memory and reaches its limit will be OOMKilled like any other. Protection from neighbours is not protection from your own bugs.
Example: two pods, one node, one memory spike
A node is nearly full. A Burstable web pod and a BestEffort scratch pod both start using more memory at the same moment. The node runs out. The kernel looks at scores, sees the BestEffort pod at the top of the list, and stops it. The web pod survives, reads from its slightly higher protection, and keeps serving users. Nobody was paged, because the plan written in advance worked.
7Node-Pressure Eviction: The Kubelet’s Polite Alternative
The kubelet would rather remove a pod gently than wait for the kernel to start swinging.
Why the kubelet acts first
The kernel’s out-of-memory killer is a last resort. It acts only when memory is already gone, and it picks processes, not whole pods, which can leave half-broken applications behind. To avoid this, the kubelet watches the node’s health continuously and starts removing pods when certain signals cross configured thresholds, ideally before the kernel has to step in. This process is called node-pressure eviction.
Memory available
How much memory is left once the working memory of the whole node is subtracted from its capacity. The most commonly triggered signal.
Node file system available
Free space, and free index entries, on the disk holding logs, writable container layers and temporary volumes.
Image file system available
Free space on the disk where container images live, if it is separate from the node file system.
Process IDs available
How many more processes the node can create before it runs out of identifiers. Fork bombs show up here.
Soft and hard thresholds
Each signal can have two kinds of threshold. A soft threshold gives the node a grace period: if the signal stays bad for the whole period, the kubelet begins evicting pods and gives them time to shut down cleanly. A hard threshold has no grace at all: the moment the signal is crossed, the kubelet evicts pods immediately, stopping them without waiting. Kubernetes ships with sensible hard defaults, for example evicting when available memory drops below roughly one hundred mebibytes, though the exact defaults vary by version and operating system.
A soft threshold is a restaurant manager saying, “We are getting full. If it stays crowded for the next ten minutes, we will ask some tables to wrap up.” A hard threshold is a fire marshal saying, “Everyone out, right now.” Both protect the building; one is polite, the other urgent.
Who gets chosen: the real ranking
Here is where many articles oversimplify. It is often said that BestEffort pods are evicted first, then Burstable, then Guaranteed. The reality is slightly different and more interesting. The kubelet ranks candidate pods using three questions, in this order.
Is the pod using more than it requested?
Pods whose usage of the starved resource exceeds their request are considered before those that stay within their request. A BestEffort pod has a request of zero, so any usage at all counts as exceeding.
How low is the pod’s priority?
Among those pods, the ones with lower priority values are chosen before those with higher priority values.
How far above its request is it?
Finally, pods are ordered by how much their usage exceeds their request. The biggest offender goes first.
Notice that the class itself is not a step in the ranking. It matters indirectly, because the class determines how likely a pod is to exceed its request. A Guaranteed pod, whose limit equals its request, can never use more than it requested, so it is almost never at the front of the queue. A Burstable pod that is quietly staying under its request is also safe, while a Burstable pod that has stretched far beyond its request is a prime target. BestEffort pods almost always exceed their zero request, so they land at the front.
flowchart TD
A["Node signal crosses a threshold"] --> B["Kubelet reclaims node-level resources such as unused images"]
B --> C{"Still under pressure?"}
C -->|"No"| D["Done, no pods removed"]
C -->|"Yes"| E["Rank pods: usage above request, then priority, then size of overuse"]
E --> F["Evict the top-ranked pod"]
F --> G{"Pressure relieved?"}
G -->|"No"| E
G -->|"Yes"| H["Stop evicting"]
If the node’s own agents use more than the room reserved for them, and only Guaranteed pods or pods within their requests are left, the kubelet must still remove something to stay healthy. In that case it chooses the lowest-priority pods first. This is why setting system and kubelet reservations honestly matters for stability.
What happens around an eviction
Evicted pods are marked as failed with a reason of Evicted, and a message that explains which resource was short. The node reports a pressure condition, such as memory pressure, for a period of time. While that condition is active, the kubelet refuses to start new BestEffort pods on that node, and the scheduler is discouraged from sending more. This protects the node from a pile-on while it recovers.
Two details surprise teams. First, node-pressure evictions do not consult PodDisruptionBudgets, the rules that limit voluntary disruptions, because the kubelet is acting to save the node, not to move workloads politely. Second, evicted pods are not restarted on the same node; a controller such as a deployment creates a replacement that the scheduler places elsewhere, so spare capacity in the cluster matters.
The kubelet checks memory at intervals. If a program grabs memory faster than the kubelet notices, the kernel’s OOM killer may act first, and the result is an OOM kill instead of a clean eviction. Careful thresholds and, where available, faster notification options reduce the gap, but never close it entirely.
8QoS, Priority and Preemption: Three Different Ideas
These three words are constantly confused. Pulling them apart will save you hours of debugging.
Quality of Service describes how firmly a pod has reserved CPU and memory. Priority is a separate number you attach to a pod through a PriorityClass to say how important it is compared with others. Preemption is an action the scheduler takes: when a high-priority pod cannot find room, it may remove lower-priority pods to make space. Each idea answers a different question, and each is controlled by different settings.
| Idea | Question it answers | How it is set | When it matters |
|---|---|---|---|
| QoS class | How firmly are resources reserved? | Derived from requests and limits | Kernel memory kills, cgroup layout, eviction likelihood |
| Priority | How important is this pod? | A PriorityClass chosen by you | Scheduling order, preemption, eviction ranking |
| Preemption | Who must make way for a pending important pod? | Done by the scheduler automatically | When the cluster is full and a high-priority pod arrives |
At a hospital, the type of ticket you paid for is like QoS: it shows what has been reserved for you. Priority is the triage nurse’s urgency score. Preemption is the nurse asking a less urgent patient to move so an emergency can use the bed. A patient can hold a very expensive ticket yet have a low urgency score, and the other way around.
They really are independent
You can have a BestEffort pod with the highest priority, a Guaranteed pod with the lowest priority, and every combination between. The scheduler’s preemption logic looks at priority and at the pod’s requests, not at its QoS class. The kernel’s memory logic looks at QoS-derived scores, not at priority. The kubelet’s eviction ranking combines usage versus requests with priority. Only by understanding all three can you predict what happens in a crisis.
High priority and Guaranteed
The strongest position. Scheduled first, can preempt others, and protected from kernel and eviction. Reserve it for truly critical services.
High priority and BestEffort
A trap. The scheduler treats it as important, but with no reservation the kernel treats it as the cheapest victim. Avoid this pairing.
Low priority and Guaranteed
A good fit for steady background work that needs reliable resources but can wait its turn for scheduling.
Low priority and BestEffort
The natural home for disposable experiments and opportunistic batch jobs that can be stopped without harm.
Built-in critical priorities
Kubernetes defines two special priority classes for essential components: one for cluster-level services and one for node-level services. Pods with these priorities are very hard to preempt and the kubelet tries hard to keep them running. They are meant for the system’s own parts, such as networking and DNS, and ordinary applications should not use them, since overuse defeats their purpose.
If every team marks its service “most important,” the number loses meaning and preemption becomes unpredictable. Keep a small, documented set of priority levels, and restrict who may use the highest ones through quotas on priority classes.
9The Scheduler’s View: Requests, Capacity and Overcommit
The scheduler is a bookkeeper. It adds up requests, ignores limits and never looks at the QoS class.
What the scheduler counts
When a new pod needs a home, the scheduler examines each node and asks whether the sum of requests of the pods already there, plus the new pod’s request, still fits within the node’s allocatable amount. If yes, the node is a candidate. Limits play no part in this arithmetic, and neither does the class. This single fact explains most “why is my node full when it looks idle?” mysteries: the node is full of promises, not of usage.
A conference organiser assigns rooms based on the number of seats each group reserved, not on how many people will really turn up. A room can look half empty while being completely booked on paper, and a new group is turned away even though the chairs are visibly free.
The gap between requests and limits is your overcommit
Because the scheduler counts only requests, the total of limits on a node can far exceed its capacity. This is overcommit, and it is deliberate. Most applications spend most of their time well below their limits, so allowing the gaps to overlap lets you pack more workloads on fewer machines. The risk is that if too many pods rise toward their limits together, the node runs short, and the QoS machinery we have studied decides who pays the price.
flowchart LR
A["Pod asks for a home"] --> B["Scheduler adds up requests on each node"]
B --> C{"Does the total fit within allocatable?"}
C -->|"Yes"| D["Node is a candidate"]
C -->|"No"| E["Node is skipped"]
D --> F["Scoring picks the best candidate"]
E --> F
F --> G["Pod is bound to the chosen node"]
How classes shape your overcommit appetite
| Class | Contribution to the scheduler’s total | Contribution to overcommit risk |
|---|---|---|
| Guaranteed | Request equals limit, so full reservation | None; it can never rise above what was counted |
| Burstable | Only the request is counted | The gap between request and limit is uncounted risk |
| BestEffort | Counted as nothing (the scheduler assumes a small default only to spread pods out) | Unbounded; can use anything free on the node |
A cluster made only of Guaranteed pods has no overcommit and no surprises, but also no sharing, so it tends to be expensive. A cluster full of BestEffort pods packs tightly but is fragile. Most healthy clusters blend them: Guaranteed for the heart of the business, Burstable for the bulk, and BestEffort for work that can be lost without regret.
The scheduler can score nodes to spread pods out or to pack them tightly, depending on configuration. Packing saves money but increases the chance of pressure on busy nodes. Choose the strategy that matches your tolerance for risk, and revisit it as your classes mix changes.
10CPU in Depth: Weights, Throttling and Exclusive Cores
CPU is forgiving, but the way it is forgiving can create some of the most puzzling performance problems in Kubernetes.
Requests are weights, limits are ceilings
When the CPU is idle, every container may use as much as it wants, unless a limit says otherwise. When the CPU is contended, the kernel divides time among containers according to their weights, which Kubernetes derives from CPU requests. A container requesting twice the CPU of its neighbour gets roughly twice the time under pressure. This is why even a Burstable pod with a tiny CPU request can become sluggish on a crowded node: its small weight gives it a small slice.
Throttling and the limits debate
CPU limits are enforced with a quota per short period, and a container that uses up its quota is paused until the next period. For programs with many threads or with sudden bursts of work, this can add small but noticeable delays to individual requests, even when the node has plenty of idle CPU. Teams running latency-sensitive services have argued for years about whether to set CPU limits at all.
Reasons to set CPU limits
- They protect neighbours from a runaway process that spins on the CPU
- They make capacity planning more predictable for shared platforms
- They are required for the Guaranteed class, with the request equal to the limit
- They help tenants in a shared cluster stay within their fair share
Reasons to skip CPU limits
- Throttling can hurt tail latency even when the node is idle
- Idle CPU goes unused instead of helping a busy pod finish faster
- CPU is compressible, so contention only slows work, it does not kill it
- Requests alone already protect each pod’s fair share under contention
There is no single right answer. A common middle path is to set CPU requests carefully, leave CPU limits off for latency-sensitive Burstable services, and keep memory limits in place because memory, unlike CPU, is not compressible. The cost of that choice is that those pods cannot be Guaranteed, since the Guaranteed class demands limits for both resources. Whatever you choose, measure throttling directly and decide with data.
A CPU limit is like a taxi meter that stops the car every few seconds once you have used your minutes for that short window, even if the road ahead is empty. A request is like a guaranteed lane during rush hour: it protects you when traffic is heavy but does not stop you from driving fast on an empty road.
Exclusive cores for the most demanding workloads
Some workloads, such as high-frequency trading, telecom packet processing and machine learning, suffer when the operating system moves their threads between processor cores. Kubernetes offers a CPU manager with a static policy that can dedicate whole cores to a container. This only applies to Guaranteed pods whose CPU request is a whole number. Such containers receive exclusive cores that no other container on the node uses, which removes interference from neighbours and cache churn.
A companion component, the topology manager, can also try to keep a container’s CPU and memory on the same hardware region, called a NUMA node, which avoids slower cross-region memory access. These features show that the Guaranteed class is not only about protection. It is also the doorway to special hardware-aware placement.
A Guaranteed pod requesting one and a half cores does not receive exclusive cores under the static policy; it shares a common pool. Only integer requests earn dedicated cores. If exclusive cores are the goal, size the request in whole cores.
Example: a trading gateway
A trading firm runs a market gateway as a Guaranteed pod requesting four whole cores. With the static CPU manager, those four cores are reserved for the container alone. Delays caused by other pods sharing the cores disappear, and response times become steady enough to meet strict internal targets.
11Memory in Depth: Working Sets, Cache and Swap
Memory numbers can be confusing, because the machine uses memory for more than your program’s own data.
What counts toward the limit
A container’s memory use includes the data your program holds, but also file cache the kernel keeps to speed up disk reads, and memory used by in-memory temporary volumes. Cache is generally reclaimable, so the kernel can drop it when pressure rises, and it does not by itself push a container over its limit. The measurement the kubelet cares about most is the working set: the memory that is truly in active use and cannot easily be taken back. Eviction decisions are based on this number, not on the raw total.
Think of your desk. The papers you are actively working on are the working set. The reference books you left lying around are like cache: they take space but can be shelved when someone needs the desk. Only the papers you cannot put away count as a real problem when the desk runs out of room.
Memory-backed temporary volumes
A common trap is a temporary volume stored in memory. It behaves like a small fast disk but its contents consume memory that counts against the container’s limit. A pod that writes large files into such a volume can be OOMKilled even though its program data is small. If your workload needs large scratch space, use disk-backed temporary storage instead.
Programs that know their limits
Language runtimes decide how much memory to claim based on what they think is available. Older runtimes read the whole machine’s memory instead of the container’s limit, and then happily grow past it. Modern versions of Java and Go can read container limits, and settings exist to cap the heap at a safe fraction of the limit. A good rule is to leave headroom between the runtime’s heap ceiling and the container’s limit, because runtimes also use memory for threads, buffers and code.
Leave headroom
Set the runtime’s heap ceiling noticeably below the container limit so that non-heap memory has room.
Match request to typical use
Set the request near the memory the program normally needs, so that both scheduling and OOM scores reflect reality.
Watch for leaks
A steadily rising working set that ends at the limit is a leak, not a sizing problem. Raising the limit only delays the failure.
Avoid memory-backed scratch
Prefer disk-backed temporary volumes for large files, to keep them out of the memory limit.
Newer memory features and swap
On nodes using the newer version of control groups, an optional feature called memory quality of service can use the memory request to protect that amount from being reclaimed and can apply soft throttling before the hard limit. It is an advanced option that has matured gradually across versions, so verify its status for your release.
Kubernetes also gained support for swap space on nodes. In the common limited mode, only Burstable pods are allowed to use swap, in proportion to their memory request. Guaranteed pods and BestEffort pods are kept out, and so are pods with high priority. The reasoning is that Guaranteed workloads want predictable performance, while BestEffort workloads have no reservation to justify a share. If you enable swap, QoS class suddenly decides who may use it.
With swap, a memory shortage may slow pods instead of killing them, which changes how you diagnose problems. Latency-sensitive systems may see slow phases that look like CPU trouble. If you enable swap, update your monitoring and runbooks to include swap activity.
12Ephemeral Storage, Process Limits and What QoS Ignores
QoS classes look only at CPU and memory. Other resources have their own rules, and mixing them up leads to nasty surprises.
The class does not care about disk
A pod can declare requests and limits for ephemeral storage, which is the temporary disk space used for the container’s writable layer, its logs and temporary volumes. These numbers matter for scheduling and for eviction, but they are not part of the QoS class calculation. A pod with perfect CPU and memory settings is Guaranteed even if it says nothing about storage, and a pod with careful storage settings but no CPU or memory settings is still BestEffort.
| Resource | Counted in the QoS class? | What happens when it runs short? |
|---|---|---|
| CPU | Yes | Throttling at limits; fair sharing by weight |
| Memory | Yes | OOM kill at limits; kernel and kubelet remove pods under node shortage |
| Ephemeral storage | No | The kubelet evicts pods that exceed their limit or fill the node disk |
| Process IDs | No | New processes fail at the pod limit; node-wide shortage triggers eviction |
| Extended resources such as GPUs | No | Pod stays pending until the device is free; not shared by default |
QoS is like a boarding class on a flight, based on your seat ticket. It tells you nothing about how much luggage you may bring, and the airline has separate rules for baggage weight. Luggage rules are the ephemeral storage and process limits: separate systems that happen to apply to the same passengers.
Disk pressure and evictions
When a node’s disk fills up, the kubelet first tries to reclaim space by removing unused images and dead containers. If that is not enough, it evicts pods, ranking them by how much they exceed their ephemeral storage requests and by priority. A noisy pod that writes endless logs or fills its temporary volume can bring down its neighbours’ ability to run, which is why setting limits on ephemeral storage is a good habit for any workload that writes data.
Process limits
Each node has a finite supply of process identifiers, and a buggy program that creates processes in a loop can exhaust them, breaking every pod on the node. Kubernetes lets operators set a limit on the number of processes per pod and monitors the node-wide supply as an eviction signal. This is a protection against a different kind of noisy neighbour, one that does not show up in CPU or memory charts until it is too late.
A full QoS strategy sets ephemeral storage limits for writers, process limits for risky workloads and quotas for special devices. The class only covers part of the picture, so do not let a green “Guaranteed” label lull you into skipping the others.
Example: the chatty logger
A Guaranteed pod with perfect CPU and memory settings writes debug logs to its container layer without limit. Over a weekend it fills the node’s disk, and the kubelet starts evicting pods on that node, including innocent neighbours. The pod’s class gave it no protection against being the cause. An ephemeral storage limit would have stopped the problem at the source.
13Configuring the Node: Reservations, Thresholds and Special Pods
QoS only works as well as the node underneath it is set up. A few kubelet settings decide how much room workloads really get.
The allocatable formula
The amount of CPU and memory that pods may use on a node is not the machine’s full size. Kubernetes starts from the node’s capacity and subtracts three things: a reservation for Kubernetes’ own components such as the kubelet and container runtime, a reservation for the operating system’s essential services, and the hard eviction threshold, which is the safety buffer the kubelet wants to keep free. What is left is the allocatable amount, and it is the budget that the scheduler spends.
Kube-reserved
Room set aside for the kubelet, the container runtime and other Kubernetes node components, so that workloads cannot starve them.
System-reserved
Room set aside for the operating system’s own services, such as the login service, the logging daemon and network tools.
Eviction thresholds
The levels of free resource at which the kubelet starts evicting pods. The hard threshold also shrinks the allocatable amount.
Enforcement of allocatable
An option that places all pods under a single control group capped at the allocatable amount, so their combined use cannot invade the reserved room.
Think of a restaurant that has one hundred seats. The owner keeps a few seats for staff meals, a few for the kitchen’s prep tables and a few empty near the door for emergencies. Only the remaining seats are sold to customers. If the owner sells all one hundred, the kitchen cannot work and the restaurant fails, even though every customer has a seat.
Why reservations are a stability feature
Without reservations, a burst of workload memory can push out the kubelet itself. When the kubelet is unhealthy, it cannot report on pods, cannot enforce eviction and may be marked unreachable by the control plane, causing a cascade of rescheduling. Setting honest reservations means the node’s own tools survive the storms that the QoS system is trying to manage. A common starting point is to reserve a small fixed amount plus a percentage that grows with node size, then adjust using measurements from your own machines.
A node with tiny reservations looks generous, because it offers a lot of allocatable room. The danger appears only under stress, when node agents are crowded out and eviction decisions go wrong. Review reservations whenever you change instance sizes or add heavy node-level agents.
Special kinds of pods and how they are counted
Init containers
Init containers run one after another before the main containers start. For scheduling, Kubernetes uses an effective request that is the larger of the biggest init container request and the combined requests of the regular containers, since they do not run at the same time. For the QoS class, however, every init container must also meet the Guaranteed rules, or the pod drops to Burstable.
Sidecar containers
Long-running helper containers, now supported as a first-class idea in recent releases, run alongside the main container for the whole life of the pod. Their requests add to the pod’s total and they count for the class. A single sidecar without limits can change the pod’s class.
DaemonSet pods
These run one copy per node and often provide essential services such as networking, monitoring and storage drivers. They deserve honest requests, memory limits and an appropriate priority, because they live on every node and are the first line of visibility during pressure.
Static pods
Static pods are managed directly by the kubelet from files on the node, such as control plane components on self-managed clusters. They have a class like any other pod and should usually be Guaranteed or have high priority, since their loss can disable the cluster.
Jobs and scheduled jobs
Batch pods are often Burstable or BestEffort. Because they end on their own, an eviction usually means a retry, which is cheap if the job is idempotent. Make sure retries are configured, so an eviction does not silently lose work.
Keep a short page that states, for each node type, its capacity, reservations, eviction thresholds and resulting allocatable amounts. When a team asks why a pod does not fit, the answer is then one lookup away.
14LimitRange and ResourceQuota: Guardrails for Namespaces
Relying on every developer to set resources correctly is hope, not a plan. Namespace rules turn good habits into defaults.
LimitRange: automatic defaults and boundaries
A LimitRange is a namespace-level rule that does two helpful things. It can fill in default requests and limits for containers that forgot to set them, and it can enforce minimum and maximum values, or a maximum ratio between a container’s limit and its request. Because it acts when the pod is created, it directly changes the QoS class the pod ends up with. A namespace with sensible defaults will almost never produce an accidental BestEffort pod.
Default request
Applied to containers that set no request, so they reserve a reasonable floor instead of nothing.
Default limit
Applied to containers that set no limit, giving every container a ceiling.
Minimum and maximum
Rejects containers that ask for too little to be useful or too much for the team’s budget.
Limit-to-request ratio
Caps how far above its request a container may stretch, which limits how much overcommit one pod can create.
A LimitRange is a school rule that every student must bring a lunch box and may not bring a lunch box bigger than a backpack. Students who forgot get a standard lunch box handed to them, and absurd requests are turned away at the door.
ResourceQuota: a budget for the whole namespace
A ResourceQuota sets a total budget for a namespace: the sum of requests and limits for CPU and memory, the number of pods, and more. When a quota covers CPU or memory, every new pod in the namespace must declare those values or be rejected, unless a LimitRange supplies defaults. The pair works well together: the quota sets the total, and the LimitRange makes sure each pod arrives with numbers attached.
Quota scopes that understand QoS
Quotas can be scoped to apply only to certain kinds of pods. Two scopes relate directly to QoS: one matches BestEffort pods and the other matches all pods that are not BestEffort. The BestEffort scope can only limit the number of such pods, since they have no resource numbers to add up. This allows a platform team to say, for example, “this namespace may run at most five BestEffort pods, but may reserve up to forty cores for everything else.”
| Control | What it does | How it affects QoS |
|---|---|---|
| LimitRange defaults | Injects requests and limits into containers that omit them | Turns would-be BestEffort pods into Burstable or Guaranteed |
| LimitRange ratio | Limits the gap between request and limit | Narrows how much a Burstable pod can overcommit |
| ResourceQuota on CPU and memory | Caps total reservations in a namespace | Forces pods to declare values, making BestEffort impossible without a default |
| BestEffort quota scope | Caps the count of BestEffort pods | Lets you permit a few disposable pods without a flood |
A LimitRange that adds a generous default limit can quietly turn a forgotten setting into a bloated reservation or a surprise class. Review what defaults produce in practice, and tell developers what the namespace applies, so the final result is never a mystery.
15QoS and Autoscaling
Autoscalers make decisions based on the numbers you wrote down. Wrong numbers lead to wrong scaling.
Horizontal scaling reads requests
The horizontal pod autoscaler adds or removes pod copies to keep a target level of utilization. When that target is a percentage of CPU or memory, the percentage is measured against the pod’s request. This has two consequences. A pod with no request, such as a BestEffort pod, cannot be scaled by utilization at all, because there is nothing to compare usage against. And a request that is far too large or too small distorts the scaling: a bloated request makes the pod look idle and prevents scaling up, while a tiny request makes it look overloaded and causes needless scale-outs.
Imagine a restaurant that hires extra waiters whenever tables are more than seventy percent full. If the manager wrote the table count wrongly, the restaurant hires too many waiters on a quiet night or too few on a busy one. The percentage is only as honest as the number it divides by, and for pods that number is the request.
Vertical scaling recommends better numbers
The vertical pod autoscaler studies real usage and recommends, or applies, new requests and limits. It is a valuable tool for finding honest numbers, especially for services whose needs drift over time. Applying new values usually means recreating pods, so plan for the brief disruption, and keep in mind that changing the numbers may change the pod’s QoS class. Use recommendation-only mode first, and review the results before letting it act automatically.
Resizing pods in place
Newer versions of Kubernetes allow certain resource changes on a running pod without restarting it. This is a big improvement for steady, stateful workloads, but it comes with an important rule: the QoS class must not change during a resize. A Guaranteed pod must remain Guaranteed, so you cannot resize it in a way that makes requests differ from limits. If the new shape would change the class, you need to replace the pod.
Node scaling follows requests too
The cluster autoscaler, and similar node provisioning tools, add machines when pods are stuck pending for lack of room. Their decisions are based on pod requests, exactly like the scheduler. They do not add nodes because pods are using a lot of memory, only because requests do not fit. If your pods ask for far more than they use, you will pay for nodes that sit mostly idle; if they ask for far less, nodes will feel crowded without any scale-out.
Measure real usage
Collect weeks of CPU and memory usage per workload, including busy peaks and quiet periods.
Set requests near typical need
Choose requests that cover normal behaviour with a small cushion, so scheduling and scaling reflect reality.
Set limits for safety
Choose memory limits that allow rare peaks, and decide deliberately about CPU limits. Equal values give Guaranteed.
Let autoscalers react
Add horizontal scaling for traffic swings, and use vertical recommendations to keep numbers honest over time.
16Choosing the Right Class for Each Workload
The right class is the cheapest one that still lets you sleep at night.
flowchart TD
A["New workload"] --> B{"Would losing it for a few minutes hurt the business?"}
B -->|"Yes, severely"| C{"Needs steady latency or exclusive cores?"}
B -->|"Somewhat"| D["Burstable with honest requests and memory limits"]
B -->|"No"| E{"Can it restart safely and run anywhere?"}
C -->|"Yes"| F["Guaranteed with whole-number CPU requests"]
C -->|"No"| G["Guaranteed or Burstable with high requests and priority"]
E -->|"Yes"| H["BestEffort or Burstable with tiny requests and low priority"]
E -->|"No"| D
The diagram is a starting point, not a law. Follow the questions: how bad is a short loss, does the workload need steady performance, and can it tolerate being restarted on another node. The answers point toward a class, and you can adjust from there based on cost and the experience of running it.
Typical workloads and their natural homes
Databases and message brokers: Guaranteed
These hold state in memory and are expensive to restart. Giving them equal requests and limits keeps neighbours from squeezing them and protects them from being chosen during node-wide shortages. Whole-number CPU requests can add stable performance.
Web servers and APIs: Burstable
Traffic rises and falls through the day. A request sized for ordinary load and a limit that allows busy spikes gives a good balance of protection and efficiency, especially when paired with horizontal autoscaling.
Machine learning training: Guaranteed
Training jobs run for hours and are sensitive to interruptions and noisy neighbours. Guaranteed pods with exclusive resources give predictable speed, and checkpointing guards against the rare failure.
Batch and report jobs: Burstable or BestEffort
Jobs that can be restarted and have flexible deadlines are ideal for using spare capacity. Low priority and small requests make them gentle neighbours that step aside when production needs room.
Developer sandboxes and experiments: BestEffort
Short-lived, disposable pods that nobody depends on can use the cheapest class. Pair it with a namespace quota on the number of such pods to avoid floods.
Node agents and daemons: Guaranteed or high-priority Burstable
Monitoring, logging and networking agents run on every node and keep the platform healthy. Give them honest requests, memory limits and a suitable priority so they survive the very pressure they help you detect.
Workloads change. A quiet internal tool can become business-critical, and an old critical service may become less important. Check the classes of your top services regularly, and align them with how much their outage would really cost today.
17Observability: Seeing Classes, Pressure and Throttling
You cannot protect what you cannot see. Make QoS visible on dashboards and in daily operations.
Finding a pod’s class
Every pod shows its class in its status. The standard description command prints it in a line labelled “QoS Class,” and a query on the pod’s status field can list classes across a whole namespace. Metrics exporters such as kube-state-metrics also publish the class as a metric, so you can group and filter dashboards by it. Make it a habit to check the class on a running pod, not just on the file you wrote, because injected containers and namespace defaults may have changed the result.
The signals worth watching
| Signal | Where it comes from | What it tells you |
|---|---|---|
| Pod QoS class | Pod status, and a kube-state-metrics series | How firmly a pod is protected |
| CPU throttling ratio | Container runtime metrics on throttled periods versus total periods | Whether CPU limits are slowing the container |
| Working set memory versus limit | Container memory metrics | How close the container is to being OOMKilled |
| Usage versus request | Container metrics compared with the pod spec | Whether requests are honest and how eviction-prone a pod is |
| Node pressure conditions | Node status | Memory, disk or process pressure that may trigger evictions |
| Eviction counts and events | Kubelet metrics and cluster events | How often pods are being removed and why |
| OOM kill events | Container last state and node kernel logs | Which containers were killed and at what memory level |
Think of a car dashboard. The speedometer, fuel gauge and engine light each warn about a different problem. Throttling is the engine feeling sluggish, working set against limit is the fuel gauge, and pressure conditions are the warning light. A driver who watches only speed will miss the fuel running out.
Useful alerts
Throttling above a threshold
Fires when a significant share of periods is throttled for a sustained time, pointing to limits that are too tight.
Memory near the limit
Fires when working set stays close to the limit, giving time to act before an OOM kill.
Node pressure condition
Fires when a node reports memory, disk or process pressure, as evictions may follow.
Unexpected BestEffort pod
Fires when a BestEffort pod appears in a production namespace, signalling a missing default or a rushed deployment.
A short weekly report listing the pods whose usage is far above or far below their requests catches most sizing problems early. Pods far above their requests are future eviction targets; pods far below are wasted money.
18Troubleshooting Playbook
When a pod disappears or crawls, a calm sequence of questions finds the cause faster than guessing.
Find out how it ended
Was the pod evicted, or was a container OOMKilled, or did it simply restart for another reason? The status reason and exit code tell you which path to follow.
Check the class and the numbers
Read the QoS class, requests and limits on the running pod, including injected containers. Compare them with real usage over the last hours.
Look at the node
Check the node’s pressure conditions, recent events and kernel logs around the time of failure. Was this one pod’s problem or a node-wide shortage?
Identify the culprit
If the node was short of memory, find which pods were far above their requests. The victim and the cause are often different pods.
Fix the numbers, not just the symptom
Adjust requests, limits, priority or placement based on evidence. Raising a limit without understanding a leak only postpones the next incident.
Symptom to cause lookup
| Symptom | Likely cause | First thing to try |
|---|---|---|
| Container restarts with reason OOMKilled and exit code 137 | Memory reached its own limit, or node memory ran out | Compare working set with the limit; check for leaks and memory-backed volumes |
| Pod shows Evicted with a message about memory | Node memory pressure and the pod ranked high for eviction | Compare its usage with its request and find the heavy neighbours |
| Pod shows Evicted with a message about ephemeral storage | Pod exceeded its storage limit or the node disk filled | Check logs and temporary volume growth; add storage limits |
| Pod stays Pending with “insufficient cpu” or “insufficient memory” | Requests do not fit any node’s allocatable room | Reduce requests if inflated, or add capacity |
| Service is slow while node CPU looks idle | CPU limit throttling | Check throttled-period ratio; raise or remove the CPU limit |
| Pod expected to be Guaranteed shows Burstable | A container, often injected, lacks matching requests and limits | Inspect every container, including init and sidecar containers |
| New pods refused on a node under pressure | The kubelet is blocking BestEffort pods during memory pressure | Give the pod a real request or let the node recover |
| Important service evicted while a batch job survived | The service exceeded its request while the job stayed within its own | Raise the service’s request and priority |
The pod that was evicted or killed may be innocent. A leaking neighbour can push a node into pressure and cause others to be removed first. Always look at who was using the most above their requests when the node got into trouble.
19Shared Clusters, Noisy Neighbours and Security
When many teams share nodes, resource settings stop being a private matter and become a question of fairness and safety.
The noisy neighbour
A noisy neighbour is a workload that consumes so much of a shared resource that others suffer. With CPU, the harm is slowness. With memory, the harm is kills and evictions. With disk and process counts, the harm is node-wide failures. QoS classes, requests, limits and quotas are the tools that keep one tenant’s problem from becoming everyone’s problem.
In an apartment block, one tenant blasting music affects the whole floor. The building rules limit noise hours, assign each flat a fair share of electricity and let the caretaker ask loud tenants to quiet down. Resource settings and quotas are the building rules for a shared cluster.
Resource settings as a security control
Unbounded workloads are an easy way to cause an outage, whether by accident or by attack. A compromised pod that spins the CPU, allocates memory without end or floods the disk with data can take down a node, and thereby harm neighbours. Memory and storage limits, process limits and quotas turn these attacks into contained incidents. Treat missing limits as a security gap, not only as an efficiency problem.
Require limits by policy
Use admission policies to reject production pods without memory limits, or to inject defaults through a namespace rule.
Cap BestEffort pods
Use the BestEffort quota scope to limit how many unreserved pods any tenant can run.
Restrict high priorities
Use quotas on priority classes so that only approved teams can run at the highest levels, preventing priority abuse.
Protect node agents
Reserve resources for system and Kubernetes components so that runaway workloads cannot starve the node itself.
Example: a quota that saved a quarter-end
A platform team gave each business unit a namespace with a quota and a LimitRange. During quarter-end reporting, one unit’s job went into a loop and tried to create thousands of pods. The pod-count quota stopped it at the namespace boundary, and other units’ critical workloads never noticed.
Resource controls reduce interference but share a kernel and a network. For strongly untrusted tenants, combine them with network policies, strict security contexts and, where needed, separate clusters or sandboxed runtimes.
20Design Patterns and Anti-Patterns
The teams that handle QoS well repeat a few good habits. The teams that suffer repeat a few famous mistakes.
Patterns that work
Defaults everywhere
Give every namespace a LimitRange and quota so no workload can arrive without resource numbers.
Honest requests from real data
Base requests on measured usage, and revisit them regularly with recommendations from autoscaling tools.
Tiered classes by business value
Guaranteed for the crown jewels, Burstable for the majority, BestEffort for the disposable, with matching priorities.
Memory limits always, CPU limits deliberately
Treat memory limits as non-negotiable safety, and decide on CPU limits per workload using throttling data.
Anti-patterns to avoid
Problem
Production pods are deployed with no requests or limits, because “it works in testing.”
Why It’s Harmful
They become BestEffort. The scheduler thinks they cost nothing, packs nodes full of them, and the first memory spike removes the most important services along with the least important.
Correct Approach
Enforce resource settings through namespace defaults and admission policy, and alert whenever a BestEffort pod appears in production.
Problem
Every service is made Guaranteed with huge requests “to be safe.”
Why It’s Harmful
The cluster fills up on paper while real usage stays low. Costs balloon, autoscalers add needless nodes, and teams still do not know what their services truly need.
Correct Approach
Reserve Guaranteed for workloads that truly justify it. Size requests from measured data and let the rest share capacity as Burstable.
Problem
A critical service has a very high priority but a tiny memory request, so it is Burstable with an almost-BestEffort OOM score.
Why It’s Harmful
The scheduler treats it as important, but the kernel treats it as cheap. Under memory pressure it is killed early, and the team is baffled because “it has the highest priority.”
Correct Approach
Align priority, request and class. A truly critical service needs an honest request, a memory limit and, ideally, the Guaranteed class.
Problem
A memory limit is raised every time a pod is OOMKilled, with no investigation.
Why It’s Harmful
If the cause is a leak, the pod simply takes longer to die, and larger limits increase overcommit and node-level risk. The real defect is never fixed.
Correct Approach
Plot working set over time. A flat line near the limit suggests undersizing; a steady climb suggests a leak that needs a code fix.
21Best Practices and Common Mistakes
If you keep only one chapter for your next design review, keep this one.
Do This
- Set memory requests and limits on every production container, including injected helpers
- Base requests on measured usage and revisit them on a regular schedule
- Use Guaranteed for stateful, latency-sensitive and business-critical workloads
- Align priority, request and class so they tell the same story about importance
- Give each namespace a LimitRange and a ResourceQuota
- Reserve capacity for the operating system and Kubernetes components on every node
- Monitor throttling, working set versus limit and node pressure conditions
Avoid This
- Deploying production pods with no resource settings at all
- Making everything Guaranteed without evidence that it is needed
- Pairing high priority with a tiny memory request
- Raising memory limits as a reflex without checking for leaks
- Assuming the scheduler looks at limits or at the class
- Ignoring ephemeral storage and process limits for chatty workloads
- Trusting the manifest instead of checking the class on the running pod
A review checklist you can reuse
Class inventory
List the QoS class of every pod in production. Question every BestEffort pod and every important service that is not Guaranteed or well-sized Burstable.
Request honesty
Compare actual usage with requests. Flag pods far above their request, which are eviction risks, and pods far below, which are waste.
Guardrails
Confirm that every namespace has defaults and quotas, and that policy blocks pods without memory limits.
Node reservations
Check that system and kubelet reservations and eviction thresholds are set sensibly, so node agents are protected.
Observability
Verify dashboards and alerts for throttling, memory against limit, node pressure and unexpected BestEffort pods.
Failure rehearsal
Practise a memory-pressure drill in a test cluster so the team has seen evictions and OOM kills before they happen in production.
Even Guaranteed pods can be stopped by node failures, upgrades, their own memory leaks or the kubelet’s last-resort choices. QoS improves the odds in resource shortages, but you still need replicas, health checks and disruption budgets for real availability.
Record why each important workload has its class and numbers, right next to the manifest. When someone later wonders why a service is Guaranteed or why its CPU has no limit, the reasoning is already there.
22Cost and Capacity Planning with QoS in Mind
Classes are not only a safety tool. They are one of the strongest levers on your cloud bill.
Where the money goes
Cloud nodes are billed by the hour whether they are busy or idle. Because the scheduler fills nodes based on requests, every unit of request you declare becomes a unit of capacity you pay for. A request that is twice what a service needs doubles that service’s cost, even though the machine sits half empty. Guaranteed pods lock in the most capacity, since their ceiling is their reservation, while Burstable and BestEffort pods let you pack more work onto the same hardware.
Renting office space works the same way. If every team insists on reserving a private room sized for its busiest day, you rent a huge building that is mostly empty. If teams share a pool of desks and only the most critical groups hold reserved rooms, you can rent a far smaller building and still keep the critical groups safe.
Four levers for balanced capacity
Right-size requests
Trim inflated requests to match measured needs, and raise tiny requests that make pods look cheaper than they are. This improves both cost and fairness.
Mix classes deliberately
Fill part of the cluster with Guaranteed pods for protection, and the rest with Burstable and BestEffort pods that soak up spare capacity.
Choose packing strategy
Decide how tightly the scheduler packs nodes. Tight packing saves money and increases the chance of pressure; looser packing is safer and costlier.
Separate node pools
Dedicate pools to critical, steady workloads and others to flexible batch work, often on cheaper interruptible machines.
A simple planning method
Classify workloads by value and tolerance
Sort services into critical, standard and disposable groups, based on what an outage would cost and how well each restarts.
Estimate the reserved base
Add up the requests of critical Guaranteed workloads. This is the capacity you must always keep available.
Estimate the flexible layer
Add typical usage of Burstable services, and decide how much headroom you want for their busy peaks.
Fill gaps with disposable work
Let low-priority, BestEffort or small-request batch jobs use what remains, knowing they can be removed first.
Test the stress case
Simulate everything rising to its limit at once, and confirm that critical workloads survive while disposable ones give way.
A useful monthly number is the gap between total requested resources and total used resources across the cluster. A large gap is money spent on promises nobody keeps; a tiny gap with frequent evictions means requests are too lean.
23Real-World and Industry Examples
The same three classes appear in very different industries, always solving the same problem of sharing without chaos.
Online retail during a sale
A retailer runs its checkout and payment services as Guaranteed pods, its product browsing as Burstable pods that scale with traffic, and its recommendation refresh jobs as low-priority BestEffort work. During a big sale, browsing traffic soars, recommendation jobs give way automatically, and checkout stays steady.
Financial trading platforms
Trading firms rely on Guaranteed pods with whole-number CPU requests and the static CPU manager to give market gateways dedicated cores. Predictable latency is worth the reserved capacity, because a few extra milliseconds can mean lost trades.
Streaming and media services
Video platforms run encoding workers as Burstable or BestEffort pods on cheaper interruptible machines, while the services that authenticate viewers and deliver playlists run as Guaranteed. If encoding nodes are reclaimed, jobs restart with no viewer impact.
Healthcare data platforms
Hospitals and health-tech companies keep patient-record services Guaranteed, while analytics and reporting jobs run as lower-priority Burstable workloads. Resource quotas per department keep one team’s heavy report from starving another team’s clinical tools.
Machine learning platforms
Teams training large models use Guaranteed pods so that long runs are not disrupted by neighbours, and run notebooks and experiments as BestEffort pods with strict quotas. Data scientists get freedom to explore, and training runs stay stable.
Managed platform teams
Internal platform groups give every product team a namespace with defaults, a quota and a documented class policy. The result is that new teams get safe behaviour without learning every detail, and the platform team can predict how the cluster behaves under stress.
24Frequently Asked Questions
No. Kubernetes derives the class from the requests and limits of the containers. To change the class, change those settings and recreate the pod, because the class cannot be edited on an existing pod.
Every container, including init and sidecar containers, must have CPU and memory limits and requests, with each request equal to its limit. If a container sets only limits, the requests default to the same values.
It is a sensible default for most services. The risk depends on how far its usage rises above its request, because that gap decides how likely it is to be chosen during pressure. Honest requests keep that risk small.
They will not be evicted because of another pod’s consumption. They can still be removed if node agents exceed their reserved resources and nothing less important remains, and they can still be OOMKilled if they exceed their own memory limit.
No. The scheduler checks that the sum of requests fits the node’s allocatable resources. It does not use limits or classes for placement. Priority, however, does influence scheduling and preemption.
OOMKilled means the Linux kernel stopped a container’s process for lack of memory. Evicted means the kubelet removed a whole pod to relieve node pressure. They have different causes, different messages and different recovery behaviour.
Not necessarily. CPU limits protect neighbours and are required for Guaranteed, but they can cause throttling that hurts latency. Many teams set CPU requests and memory limits, and add CPU limits only where there is a clear reason. Measure throttling to decide.
Most often because a container added by a mesh, security or logging tool lacks matching limits and requests, or because a LimitRange changed a value. Inspect all containers on the running pod, not just your manifest.
No. The class is computed only from CPU and memory. Ephemeral storage has its own requests, limits and eviction rules, and devices such as GPUs are handled through extended resources.
They are independent. Priority says how important a pod is for scheduling, preemption and eviction ranking. QoS describes how firmly resources are reserved and influences kernel kill preference. Align them for important workloads.
No. In-place resizing, where available, must keep the same QoS class. If you want a different class, replace the pod with a new one that has the new settings.
The scheduler counts requests, not usage. A node whose pods have requested all its allocatable room is full on paper, even if real usage is low. Right-sizing requests frees that room.
Yes, for disposable and experimental work that can be stopped without harm, ideally with low priority and a quota on how many such pods a namespace may run. It is a poor choice for anything users depend on.
Horizontal scaling by utilization measures usage against requests, so pods without requests cannot use it. Node autoscalers add machines when requests cannot be placed. Honest requests make both work properly.
Combine a LimitRange for defaults, a ResourceQuota for budgets, and an admission policy that rejects production pods without memory limits. Add alerts for unexpected BestEffort pods and a regular review of usage against requests.
25Summary and Key Takeaways
Kubernetes nodes are shared and deliberately overcommitted, so there will be moments when the promises made cannot all be kept. Quality of Service classes are the plan that decides what happens in those moments. Guaranteed pods have reserved exactly what they use and are protected most, Burstable pods have a reserved floor and room to stretch, and BestEffort pods have reserved nothing and give way first. The class is derived automatically from requests and limits, turned into control groups and kernel scores, and combined with priority and usage to guide the kubelet’s evictions. Treat the numbers you write as promises, keep them honest, and use classes to spend protection where an outage would hurt and to share capacity everywhere else.
Key Takeaways
- Three derived classes — Guaranteed, Burstable and BestEffort are computed from CPU and memory requests and limits across every container, and cannot be set directly or changed on a running pod.
- Requests drive scheduling, limits drive enforcement — the scheduler counts only requests, while the kernel enforces limits through throttling for CPU and kills for memory.
- The kernel and kubelet use classes differently — QoS shapes the control group layout and OOM scores, while eviction ranks pods by usage above requests, then priority, then size of overuse.
- Priority is separate from QoS — align them for important workloads, and never pair high priority with a tiny memory request.
- Memory is unforgiving, CPU is flexible — always set memory limits, and decide on CPU limits deliberately using throttling data, remembering that Guaranteed requires them.
- Guardrails beat good intentions — LimitRange defaults, ResourceQuota budgets and admission policies prevent accidental BestEffort pods and noisy neighbours.
- Observe and revisit — watch throttling, working set against limit and node pressure, check the class on running pods, and review requests regularly as workloads change.