Pods keep dying in k8s, not crashing but being killed. They work fine and then disappear, k8s creates new ones and the logs doesn't give any useful info. That's sth that k8s does on purpose, as you haven't ever told it how much memory your app needs. Until you do, it keeps killing them.
Why Kubernetes evicts pods
k8s runs on nodes — physical or virtual machines that host your containers — and each node has a finite amount of CPU and memory. When a node runs low on resources, k8s needs to decide which pods stay and which are being evicted to free up some space.
That decision comes down to QoS classes — quality-of-service tiers k8s assigns every pod based on how you've set its resource requests and limits. There are three:
- BestEffort — no resource requests or limits defined. k8s has no idea how much CPU or memory the pod needs. These get killed first.
-
Burstable — requests and limits are defined, but they're different (e.g.,
requests: 256Mi,limits: 512Mi). The pod is guaranteed the request amount, but can burst up to the limit. Killed second. - Guaranteed — requests and limits are set to the same value. k8s reserves exactly that amount of resources for the pod. Killed last.
If you don't have any resource configuration for your pods, they run as BestEffort. And when a node hits memory pressure, BestEffort pods are the first ones to go, no questions asked.
The Guaranteed class
Setting up a pod to get into the Guaranteed class is as easy as adding the following to your deployment configuration — you define requests and limits for both CPU and memory and set them to be identical:
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "512Mi"
cpu: "500m"
k8s knows that this pod needs exactly that much of RAM and half of a CPU. That's why it reserves that capacity when it schedules it on the node. If some node doesn't have 512 MiB free, the pod won't be placed there. And if the node will run into memory pressure later, this pod is being evicted last — after every BestEffort and Burstable pod.
The side effect — better autoscaling
Another advantage of managed k8s platforms like EKS is that the Cluster Autoscaler looks at resource requests when deciding whether to add new nodes. If your pods are set to BestEffort with no resource configuration, the autoscaler thinks they need zero resources. So ten pods on one node is all good for it — even if that node is at 90 percent memory, it won't create another node, as it doesn't see any demand. But if those same pods have a Guaranteed setting with 512Mi requests, and the node has less than 512 MiB free, the autoscaler will see a pod it can't schedule, so it adds a new node, and your pods spread across multiple nodes instead of piling up on one.
It's especially strict on EKS — other providers are more lenient, but EKS follows the scheduler's resource math to the letter. So if you have no requests, the autoscaler will never trigger, and your pods will cram onto a single node until it runs out of memory and starts evicting.
The trade-off
The downside of using Guaranteed is that you need to commit to some memory limit, so if your app grows and uses more memory than you have set, then instead of being burstable it will get OOMKilled by k8s — so with Burstable, for example, you could do 256Mi requests and 1Gi limit to give your app some room to spike, but in return you lose the scheduling guarantee, as k8s reserves only 256 MiB, so the pod might end up on a node which doesn't have a full gigabyte free. But with Guaranteed you just need to keep an eye on how much memory your app uses and occasionally increase the limit — a little more maintenance, but in exchange you get predictable scheduling, protection against eviction, and autoscaling which actually works.
How to set it
In your Kubernetes deployment manifest, add the resources block under containers:
apiVersion: apps/v1
kind: Deployment
metadata:
name: your-app
spec:
replicas: 3
template:
spec:
containers:
- name: app
image: your-image:latest
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "512Mi"
cpu: "500m"
Apply it:
kubectl apply -f deployment.yaml
Check the QoS class:
kubectl get pod <pod-name> -o jsonpath='{.status.qosClass}'
If it says Guaranteed, you're set.
Picking the right values
Start by looking at what your pods are actually using. Get current memory consumption:
kubectl top pods
Take the highest memory value you see, add 20–30% of headroom, and use this for both request and limit. If a pod is sitting at 400 MiB set it to 512 MiB; if it's consistently hitting 800 MiB then go with 1 GiB. For CPU, half a core (500m) is a good starting point for most apps — increase it if your metrics are showing throttling on the CPU level. Then monitor: OOMKills in the pod events mean the limit is too low, so you raise it. And if memory usage is growing over time as you ship new features, just update the config to match.
Once your pods are Guaranteed, k8s stops killing them at random — but you still have to tell it what Guaranteed actually means. That's the entire job of requests and limits.
Top comments (0)