Ever since module 2 we have been writing resources blocks in every Rutas Norte manifest — cpu: 250m, memory: 256Mi — without ever explaining what they mean exactly or where those numbers come from. The previous lesson even exposed them to the container with resourceFieldRef so that bookings-api could size itself. It is time to take them seriously, because they are the piece that decides which node each pod fits on, how much CPU it gets when there is competition and which process dies when memory runs short. And we are still carrying the last module 2 debt: no namespace has a quota, so right now a wrong deployment in rutas-norte-dev can eat the cluster capacity and leave the production pods with nowhere to go. In this lesson you will understand the complete resource model and you will put a ResourceQuota on each of the three environments.

Contents

  1. The resource model: requests and limits
  2. Units: millicores, Mi versus M
  3. Who uses requests: the scheduler
  4. Who uses limits: the kubelet and cgroups
  5. Compressible CPU versus incompressible memory
  6. Diagnosing CPU throttling
  7. Diagnosing an OOMKilled
  8. ephemeral-storage and disk eviction
  9. Cluster overcommitment
  10. ResourceQuota: what it limits and how to read it
  11. The effect that forces you to declare resources
  12. The quotas for the three Rutas Norte environments
  13. How to choose sensible values

  1. The resource model: requests and limits

Every container in a pod can declare two numbers for each type of resource:

        - name: api
          image: registry.rutasnorte.example/bookings-api:2.5.0
          resources:
            requests:                 # what the container NEEDS GUARANTEED
              cpu: 250m
              memory: 256Mi
            limits:                   # the CEILING it cannot exceed
              cpu: "1"
              memory: 512Mi

The conceptual difference, which is the basis of everything:

requests limits
Meaning "Reserve this for me" "Do not let me go past this"
Who uses it The scheduler, when picking a node The kubelet, at runtime
When it acts Once, when the pod is created Continuously, for as long as the pod lives
If the node does not have it The pod stays Pending
If it is exceeded It cannot be exceeded: it is guaranteed CPU: throttling. Memory: OOMKilled
Affects the cluster bill Yes: it is committed capacity Indirectly
If omitted Assumed to be 0 (dangerous) Assumed unlimited (dangerous)

A useful analogy, and one that fits Rutas Norte: think of a coach in the fleet. The request is the reserved seat: it is paid for and nobody else can take it, whether you use it or not. The limit is the maximum luggage allowance: you can carry less, but if you try to go over the maximum they stop you at the door.

And the two consequences to fix in your mind from the outset:

  1. requests is what "costs" in the cluster. A pod with requests: 4Gi blocks 4 GiB of node capacity even if its process uses 100 MiB. The scheduler treats that node as having 4 GiB less available for everything else.
  2. limits reserves nothing. A pod with limits: 8Gi does not reserve 8 GiB; it simply will not be able to go past that if it tries to use them.

You can declare only requests, only limits, both or neither:

Declaration Effect
Both, with requests < limits The usual case: a minimum guarantee with room to burst
Both, equal Maximum predictability and maximum priority (you will see it in 03-05)
Only limits Kubernetes copies the limit into the request. Careful: you reserve more than you think
Only requests No ceiling: the container can consume the whole node
Neither The worst case: no guarantee and no ceiling

  1. Units: millicores, Mi versus M

CPU

The CPU unit is the core (more precisely, a thread of execution: a vCPU in the cloud, a hyperthread on bare metal). It can be expressed in two ways:

Written as Meaning
1 or 1000m A whole core
500m Half a core
250m A quarter of a core
100m A tenth of a core
0.5 Equivalent to 500m, but discouraged
1m The minimum accepted

The m stands for milli: 1000m = 1 core. The m form is preferred because it avoids floating-point errors and because 0.1 can suffer representation problems. Always write millicores.

CPU is fractional and elastic: if you ask for 250m you are not given a quarter of a physical processor but a quarter of the CPU time available in a time window. And if the node is idle, you can use more (up to your limit).

Memory

Memory is measured in bytes and accepts two families of suffixes that are not equivalent:

Suffix Base Value Example
Ki 2 1,024 512Ki = 524,288 bytes
Mi 2 1,048,576 256Mi = 268,435,456 bytes
Gi 2 1,073,741,824 1Gi = 1,073,741,824 bytes
Ti 2 2⁴⁰
k 10 1,000 512k = 512,000 bytes
M 10 1,000,000 256M = 256,000,000 bytes
G 10 1,000,000,000 1G = 1,000,000,000 bytes

256M is 6.9% less memory than 256Mi. And 1G is 74 MiB less than 1Gi. In a memory limit, that 6.9% is exactly the difference between a container that just about copes and one that dies with OOMKilled under load.

Rule for Rutas Norte and for any serious project: always use Mi and Gi, never M or G. It is what every Kubernetes tool does and what anyone reading your manifest expects.

And a typo that costs dearly:

            limits:
              memory: 512m           # <- LOWER CASE: 512 MILLIbytes = 0.512 bytes

Kubernetes accepts it without complaint: m is a valid suffix (milli). The container gets a limit of half a byte and dies instantly. The symptom is a baffling CrashLoopBackOff. m only makes sense for CPU.

kubectl describe pod bookings-api-x -n rutas-norte-dev | grep -A4 Limits
    Limits:
      memory:  512m
    Requests:
      memory:  512m

If you see that, you already know what the problem is.

  1. Who uses requests: the scheduler

The kube-scheduler you studied in module 1 has one job: pick a node for each new pod. And the decision is based exclusively on requests, not on actual usage.

flowchart TB
    A["New pod:<br/>requests cpu=250m mem=256Mi"] --> B["FILTERING<br/>Which nodes have free ALLOCATABLE capacity?"]
    B --> C{"node-1<br/>free: 200m CPU"}
    B --> D{"node-2<br/>free: 1500m CPU"}
    B --> E{"node-3<br/>free: 800m CPU"}
    C -->|"does NOT fit"| F["Discarded"]
    D -->|"fits"| G["SCORING"]
    E -->|"fits"| G
    G --> H["Chosen: the best score"]
    H --> I["The pod is BOUND to the node<br/>spec.nodeName"]

The calculation the scheduler makes for each node is:

free = allocatable - sum of the REQUESTS of every pod already assigned to that node

Two critical nuances:

Nuance 1: allocatable is not the node's total capacity. Kubernetes reserves a portion for the operating system and for its own components (kubeReserved, systemReserved, evictionHard).

kubectl describe node rutas-norte | grep -A8 "Capacity:"
Capacity:
  cpu:                4
  ephemeral-storage:  61202244Ki
  memory:             8039484Ki
  pods:               110
Allocatable:
  cpu:                4
  ephemeral-storage:  56403448552
  memory:             7937084Ki
  pods:               110

In this minikube the difference is small, but on a managed cloud node it can be 10-15% of the memory. Always plan against Allocatable, not against Capacity.

Nuance 2: requests are summed, not actual usage. This is the source of the most common confusion. A node can be at 8% actual CPU usage and still reject a new pod because the sum of the requests of what is already assigned leaves no room.

kubectl describe node rutas-norte | grep -A12 "Allocated resources"
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests      Limits
  --------           --------      ------
  cpu                2350m (58%)   5200m (130%)
  memory             2816Mi (36%)  4608Mi (59%)
  ephemeral-storage  0 (0%)        0 (0%)

Read it carefully: the CPU Requests add up to 58% of allocatable, but the Limits add up to 130%. That is the overcommitment of section 9, and that note in brackets warns about it explicitly.

When there is no room on any node, the pod stays Pending:

kubectl get pods -n rutas-norte-dev
kubectl describe pod bookings-api-6d8f7c-abc -n rutas-norte-dev | tail -5
NAME                       READY   STATUS    RESTARTS   AGE
bookings-api-6d8f7c-abc    0/1     Pending   0          2m14s

Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  2m    default-scheduler  0/3 nodes are available:
    3 Insufficient memory. preemption: 0/3 nodes are available: 3 No preemption victims found.

Insufficient memory with the pod in Pending always means the same thing: the requests do not fit. The fixes are lowering the requests (if they were inflated), adding a node, or waiting for the cluster autoscaler to do it for you.

A warning about inflated requests: it is tempting to ask for more "just in case". But every MiB of request you do not use is cluster capacity nobody can use and that you are paying for. In real clusters it is normal to find 30-40% of capacity committed and unused purely because of badly calibrated requests.

  1. Who uses limits: the kubelet and cgroups

The scheduler never sees limits. They are applied by the kubelet on the node, and not on its own: it asks the Linux kernel through cgroups (control groups), the mechanism that allows the resources of a set of processes to be limited.

We can see it from inside the container. With limits: cpu: "1" and memory: 512Mi on cgroups v2:

kubectl exec -n rutas-norte-dev deploy/bookings-api -- sh -c \
  'cat /sys/fs/cgroup/memory.max; cat /sys/fs/cgroup/cpu.max; cat /sys/fs/cgroup/cpu.weight'
536870912
100000 100000
10

Let us decode the three lines, because they explain the whole behaviour:

  • memory.max = 536870912: that is exactly 512 MiB. It is a hard ceiling. If the process tries to allocate byte 536870913, the kernel fires the OOM killer.
  • cpu.max = 100000 100000: these are quota and period in microseconds. It means "100,000 µs of CPU every 100,000 µs", that is, a whole core. With limits: cpu: 500m it would be 50000 100000.
  • cpu.weight = 10: it derives from the CPU request (250m). It is the relative weight with which the kernel scheduler shares out CPU time when there is competition. A container with twice the request receives twice the time.

That last line is important and little known: the CPU request is not only for the Kubernetes scheduler; it also determines the relative priority within the node. Two pods on the same node competing for CPU share the time in proportion to their requests.

  1. Compressible CPU versus incompressible memory

This is the most important distinction in the lesson and the one you really have to understand.

CPU Memory
Type of resource Compressible Incompressible
Can be taken away live Yes: just give it less time No: what is allocated is allocated
When the limit is exceeded Throttling OOMKilled: the process dies
Consequence The process runs slower The container restarts and work in progress is lost
Visible in container_cpu_cfs_throttled_seconds metrics kubectl describe pod, RESTARTS
Severity High latency, timeouts Lost requests, service outage

CPU: throttling. If your container has limits: cpu: 500m and tries to use more, the kernel simply gives it no more CPU time in that 100 ms period. The process does not die: it waits. The visible effect is latency. For bookings-api this means that a request that took 80 ms starts taking 400 ms, and if the TIMEOUT_MS of web-store is set to 2000, with enough throttling timeout errors start appearing in the customer's browser.

Memory: death. There is no way to "throttle" memory. If a process has allocated 512 MiB and asks for more, the kernel cannot take anything back: either it gives it memory or it kills somebody. With limits: memory: 512Mi, it kills the process that went over, inside the container's cgroup. The container dies with exit code 137 and the kubelet restarts it according to the restartPolicy.

The practical design consequence:

Memory limits have to be calibrated carefully, because falling short kills the process. CPU limits are far less dangerous, but also far more debatable.

And here there is a genuine debate in the community that is worth knowing about: should you set CPU limits or not?

In favour of a CPU limit Against a CPU limit
It stops a runaway process degrading its neighbours It throttles even when the node is idle: capacity is wasted
It makes performance predictable across environments It can cause throttling during short start-up bursts
It lets you detect sooner that the application needs more With threads, throttling affects the whole process, not just the guilty thread
Required for the Guaranteed class (03-05) Many large teams deliberately omit them

The Rutas Norte position, which is the recommendable one for a team that is starting out:

  • CPU and memory requests: always, on every container. Not negotiable.
  • Memory limits: always. A pod with no memory limit can bring down the whole node and cause cascading evictions.
  • CPU limits: yes in dev and pre (to spot excessive consumption early) and generous in pro, between 2 and 4 times the request, so that bank-holiday and school-holiday peaks can be absorbed.

  1. Diagnosing CPU throttling

Throttling is treacherous because it does not show up in any pod state. The pod is Running, READY 1/1, with no restarts, and it still responds badly.

The signal is in the cgroup itself:

kubectl exec -n rutas-norte-pro deploy/bookings-api -- cat /sys/fs/cgroup/cpu.stat
usage_usec 184920331
user_usec 152014882
system_usec 32905449
nr_periods 918442
nr_throttled 214883
throttled_usec 41220984

How to read it:

  • nr_periods: how many 100 ms periods have elapsed.
  • nr_throttled: in how many of them the container used up its quota and was throttled.
  • throttled_usec: total microseconds spent waiting.

The key indicator is the ratio:

throttling = nr_throttled / nr_periods = 214883 / 918442 = 23.4%

Almost a quarter of the periods have been throttled. Interpretation:

Ratio Diagnosis
< 1% Normal. Occasional bursts
1-5% Keep an eye on it. Acceptable in batch processes
5-25% A real latency problem. Raise the limit
> 25% Serious. The limit is clearly set wrong

A command to review every pod of a component:

for P in $(kubectl get pods -n rutas-norte-pro -l app=bookings-api -o name); do
  echo "--- $P"
  kubectl exec -n rutas-norte-pro "$P" -- sh -c \
    'awk "/nr_periods|nr_throttled/ {print \$1, \$2}" /sys/fs/cgroup/cpu.stat'
done
--- pod/bookings-api-7f4b8c9d6-2xkqp
nr_periods 918442
nr_throttled 214883
--- pod/bookings-api-7f4b8c9d6-8mvtr
nr_periods 917210
nr_throttled 198445

The proper way to monitor this continuously is with metrics: the container_cpu_cfs_throttled_periods_total series from cAdvisor, which you will see in Monitoring with Prometheus. The command above is for a one-off diagnosis.

The fix: raise the CPU limit. If bookings-api has requests: 250m and limits: 500m with 23% throttling, raising the limit to "1" usually solves it at no real cost, because the limit reserves no capacity.

  1. Diagnosing an OOMKilled

The opposite case is loud and easy to identify:

kubectl get pods -n rutas-norte-pro -l app=bookings-api
NAME                           READY   STATUS      RESTARTS      AGE
bookings-api-7f4b8c9d6-2xkqp   1/1     Running     4 (2m ago)    41m
bookings-api-7f4b8c9d6-8mvtr   0/1     OOMKilled   3 (18s ago)   41m

The detail is in describe:

kubectl describe pod bookings-api-7f4b8c9d6-8mvtr -n rutas-norte-pro
Containers:
  api:
    State:          Waiting
      Reason:       CrashLoopBackOff
    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137
      Started:      Wed, 05 Aug 2026 23:12:04 +0200
      Finished:     Wed, 05 Aug 2026 23:14:41 +0200
    Restart Count:  3
    Limits:
      cpu:     1
      memory:  512Mi
    Requests:
      cpu:     250m
      memory:  256Mi

The three unmistakable clues: Reason: OOMKilled, Exit Code: 137 (which is 128 + 9, the SIGKILL signal) and a Restart Count that keeps growing.

Careful with Last State: it describes the previous container. If the pod is Running right now but has RESTARTS 4, Last State tells you why it died last time. It is the first thing to look at when facing a pod with restarts.

And a warning about kubectl logs: when the container restarts, the logs start from scratch. To see those of the dead container, which are the ones holding the clue:

kubectl logs bookings-api-7f4b8c9d6-8mvtr -n rutas-norte-pro --previous | tail -6
2026-08-05T23:14:38.221Z INFO  [pod=bookings-api-7f4b8c9d6-8mvtr] availability_query
  route=Bilbao-Santander dates=2026-08-14..2026-08-17 results=8412
2026-08-05T23:14:40.887Z WARN  [pod=bookings-api-7f4b8c9d6-8mvtr] heap 478MB / 512MB

There is the cause: an availability query for a bank-holiday weekend returned 8,412 results and the application loaded them all into memory.

The five typical causes of an OOMKilled and how to treat them:

Cause Signal Fix
Limit too low It always dies under normal load Raise limits.memory
Memory leak It dies periodically, faster each time Fix the code; meanwhile, raising the limit delays the problem, it does not solve it
One-off spike (large query) It only dies with certain requests Paginate the query, cap the results
Runtime that cannot see the limit The JVM or Node allocate based on the node's RAM -XX:MaxRAMPercentage or --max-old-space-size with resourceFieldRef (03-03)
Wrong container The one dying is the sidecar, not the app Look at the container name in describe

The fourth deserves a note, because it is the most frequent and the most invisible: a Java process without MaxRAMPercentage on a 32 GiB node with a limit of 512 MiB sizes its heap for the node's 32 GiB and dies as soon as it starts working. The previous lesson already gave you the tool to fix it.

  1. ephemeral-storage and disk eviction

There is a third resource that almost nobody declares and that causes baffling incidents: ephemeral storage, that is, the node disk the container uses for:

  • The write layer of its file system (anything it writes outside a volume).
  • emptyDir volumes.
  • The container logs (whatever it writes to stdout/stderr, which the kubelet stores on the node's disk).

It is declared just like the others:

          resources:
            requests:
              cpu: 250m
              memory: 256Mi
              ephemeral-storage: 1Gi
            limits:
              cpu: "1"
              memory: 512Mi
              ephemeral-storage: 2Gi

What happens when the limit is exceeded: the kubelet evicts the pod, with Reason: Evicted:

kubectl get pods -n rutas-norte-pro | grep Evicted
kubectl describe pod notifications-worker-6f7d9-abcde -n rutas-norte-pro | grep -A3 "Status:"
notifications-worker-6f7d9-abcde   0/1   Evicted   0   34m

Status:   Failed
Reason:   Evicted
Message:  Pod ephemeral local storage usage exceeds the total limit of containers 2Gi.

A relevant nuance: unlike an OOMKilled, disk eviction is not immediate. The kubelet checks usage roughly every 10 seconds, so a process can fill the disk before it gets evicted.

And the bigger problem: if the node's disk fills up, the kubelet goes into pressure and evicts pods even when their limits are in order. It is a failure that affects the whole node. The signals:

kubectl describe node rutas-norte | grep -A6 Conditions
Conditions:
  Type             Status  Reason                       Message
  ----             ------  ------                       -------
  MemoryPressure   False   KubeletHasSufficientMemory   kubelet has sufficient memory available
  DiskPressure     True    KubeletHasDiskPressure       kubelet has disk pressure
  PIDPressure      False   KubeletHasSufficientPID      kubelet has sufficient PID available
  Ready            True    KubeletReady                 kubelet is posting ready status

DiskPressure: True explains apparently random evictions. The order in which the kubelet picks its victims depends on the QoS class, which is the subject of the next lesson.

In Rutas Norte, the natural candidate to fill the disk is notifications-worker: if it produces one log line per email sent and tens of thousands are sent over a bank-holiday weekend, without log rotation the node's disk fills up. The practical rule: declare ephemeral-storage on any component that writes bulky logs or uses emptyDir, and keep an eye on DiskPressure on the nodes.

  1. Cluster overcommitment

Let us go back to that earlier line:

  Resource  Requests      Limits
  cpu       2350m (58%)   5200m (130%)

The sum of the CPU limits is 130% of the node's allocatable. Is the cluster misconfigured? No: that is normal and even desirable.

flowchart TB
    subgraph N["Node: 4 allocatable cores"]
        R["Committed REQUESTS: 2350m<br/>(guaranteed, the scheduler never exceeds them)"]
        L["Summed LIMITS: 5200m<br/>(the scheduler does NOT look at them)"]
    end
    R --> S["Safe: the scheduler never commits<br/>more requests than capacity"]
    L --> P["Overcommitment: only a problem<br/>if they ALL spike at once"]

The logic of overcommitment: pods do not consume their maximum simultaneously. notifications-worker works in bursts, bookings-api peaks at specific hours, web-store serves static assets with little consumption. Allowing the sum of limits to exceed capacity takes advantage of those gaps.

Where the danger lies, depending on the resource:

Resource If they all ask at once
CPU They are all throttled in proportion to their request. High latency, no crashes. Recoverable
Memory The node runs out of memory. The kubelet evicts pods. If it happens very fast, the kernel's OOM killer kills processes. Real crashes

The operational conclusion is asymmetric and worth engraving:

Overcommitting CPU is acceptable and normal. Overcommitting memory aggressively is dangerous.

For Rutas Norte, the recommended ratios:

Resource limit / request ratio Reason
CPU Between 2 and 4 Absorb bank-holiday peaks without wasting capacity
Memory Between 1 and 1.5 The tighter it is, the lower the risk of cascading evictions
Memory of bookings-postgres Exactly 1 (equal) A database must never be an eviction candidate

That last case, with requests == limits, has a name and very concrete consequences for who dies first: it is the Guaranteed class, and it is the subject of the next lesson.

And a memory warning that surprises many people: Kubernetes disables swap by default (although since 1.30 there is configurable beta support). With no swap, the kernel has no buffer: when memory runs out, it runs out. It is one more reason not to overcommit it.

  1. ResourceQuota: what it limits and how to read it

Everything above is per container. The ResourceQuota acts at another level: it puts a ceiling on a whole namespace. It is the object that settles the last module 2 debt.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: quota-dev
  namespace: rutas-norte-dev
spec:
  hard:
    # --- Compute ---
    requests.cpu: "2"                    # sum of the CPU requests of every pod
    requests.memory: 4Gi
    limits.cpu: "4"                      # sum of the limits
    limits.memory: 8Gi
    requests.ephemeral-storage: 10Gi

    # --- Object counts ---
    pods: "20"
    services: "10"
    configmaps: "20"
    secrets: "20"
    persistentvolumeclaims: "5"
    services.loadbalancers: "0"          # creating LoadBalancers is banned in dev
    services.nodeports: "0"
    count/deployments.apps: "10"
    count/jobs.batch: "10"
    count/cronjobs.batch: "5"

    # --- Storage ---
    requests.storage: 20Gi               # sum of what every PVC asks for

The three families of limits:

Family Examples What it controls
Compute requests.cpu, limits.memory That one environment does not consume all the capacity
Object counts pods, services, secrets That nobody saturates etcd or the control plane
Storage requests.storage, persistentvolumeclaims The cost of the disks (module 5)

Points that define its behaviour:

  • It is a namespaced object and only affects its namespace. There are no cluster-wide quotas.
  • It is applied at creation time. A pod that would exceed the quota is rejected; those that already exist are untouched.
  • There can be several ResourceQuotas in a namespace. All of them apply and all of them must be satisfied.
  • Terminated pods do not count. Those in Succeeded or Failed are excluded from the tally.
  • services.loadbalancers: "0" is a very useful cost-control tool: every cloud LoadBalancer costs money.

Reading a quota:

kubectl describe resourcequota quota-dev -n rutas-norte-dev
Name:                     quota-dev
Namespace:                rutas-norte-dev
Resource                  Used    Hard
--------                  ----    ----
configmaps                6       20
count/cronjobs.batch      0       5
count/deployments.apps    5       10
limits.cpu                2300m   4
limits.memory             3200Mi  8Gi
persistentvolumeclaims    1       5
pods                      9       20
requests.cpu              1150m   2
requests.memory           1600Mi  4Gi
secrets                   4       20
services                  4       10
services.loadbalancers    0       0

Two columns: Used (what is consumed right now) and Hard (the ceiling). This namespace is at 57% of its CPU requests and 40% of its memory. It is the view that answers "how much headroom do I have left in development?".

When somebody tries to go over:

kubectl scale deploy/bookings-api --replicas=12 -n rutas-norte-dev
kubectl get events -n rutas-norte-dev --sort-by=.lastTimestamp | tail -3
deployment.apps/bookings-api scaled

LAST SEEN   TYPE      REASON         OBJECT                            MESSAGE
3s          Warning   FailedCreate   replicaset/bookings-api-6d8f7c9b   Error creating: pods
  "bookings-api-6d8f7c9b-" is forbidden: exceeded quota: quota-dev, requested: requests.cpu=250m,
  used: requests.cpu=1900m, limited: requests.cpu=2

Pay attention to a detail that is very important for diagnosis: the kubectl scale command succeeded. The Deployment now says 12 replicas. What fails is the creation of the pods by the ReplicaSet, and that failure does not appear in kubectl get deploy other than as a READY 8/12 that lasts for ever.

kubectl get deploy bookings-api -n rutas-norte-dev
kubectl describe deploy bookings-api -n rutas-norte-dev | grep -A4 Conditions
NAME           READY   UP-TO-DATE   AVAILABLE   AGE
bookings-api   8/12    12           8           3h

Conditions:
  Type             Status  Reason
  ----             ------  ------
  Available        True    MinimumReplicasAvailable
  ReplicaProgressing False ProgressDeadlineExceeded

Faced with a Deployment stuck at READY x/y, always look at the ReplicaSet events and the namespace quota. It is one of the most frequent and least obvious causes, and it connects directly with the diagnosis of stuck rollouts you saw in module 2.

Scopes

A quota can be applied to only a subset of pods:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: high-priority-quota
  namespace: rutas-norte-pro
spec:
  hard:
    requests.cpu: "8"
    requests.memory: 16Gi
  scopeSelector:
    matchExpressions:
      - operator: In
        scopeName: PriorityClass
        values: ["high"]

Available scopes: Terminating and NotTerminating (depending on whether they have activeDeadlineSeconds), BestEffort and NotBestEffort (depending on the QoS class from the next lesson), and PriorityClass. They let you give more headroom to the critical than to the experimental within the same namespace.

  1. The effect that forces you to declare resources

Here is the most useful and at the same time most disconcerting behaviour of ResourceQuotas:

If a namespace has a quota limiting requests.cpu or limits.memory, then EVERY pod created in it must declare that resource. If it does not, it is rejected.

The logic is obvious as soon as you think about it: to tally the sum of the namespace's requests.cpu, Kubernetes needs every pod to declare requests.cpu. A pod that did not declare it would make the tally impossible.

Demonstration. With the dev quota active, we try to create a pod with no resources:

kubectl run test-no-resources --image=nginx:1.27.1-alpine -n rutas-norte-dev
Error from server (Forbidden): pods "test-no-resources" is forbidden: failed quota: quota-dev:
  must specify limits.cpu for: test-no-resources; limits.memory for: test-no-resources;
  requests.cpu for: test-no-resources; requests.memory for: test-no-resources

This is, by some distance, the most valuable side effect of quotas: it turns the discipline of declaring resources into something mandatory at platform level, rather than a recommendation people forget.

But it has an immediate cost: it breaks everything convenient. An ephemeral pod for debugging, a quick kubectl run, a one-off Job... they all fail. And this is where the missing piece appears, which is the subject of the next lesson: a LimitRange in the namespace injects default values into containers that declare nothing, so the pod passes the quota validation without anyone writing resources by hand.

flowchart LR
    A["Pod with no resources"] --> B{"Is there a LimitRange<br/>in the namespace?"}
    B -->|"Yes"| C["default and defaultRequest<br/>are injected"]
    B -->|"No"| D["The pod still has<br/>no resources"]
    C --> E{"Is there a compute<br/>ResourceQuota?"}
    D --> E
    E -->|"Yes, and the pod declares"| F["Tallied. ACCEPTED"]
    E -->|"Yes, and it does not declare"| G["REJECTED<br/>must specify requests.cpu"]
    E -->|"No"| F

ResourceQuota and LimitRange are inseparable. Setting a quota without a LimitRange is a defensible decision (it forces everyone to think about their resources), but it turns day-to-day work into a constant nuisance. The combination of the two is the standard configuration of any serious namespace.

  1. The quotas for the three Rutas Norte environments

Now for real: we apply the quotas and the debt is settled. The criterion is that dev must not be able to do harm and pro must have headroom for bank-holiday and school-holiday peaks.

rutas-norte-dev

apiVersion: v1
kind: ResourceQuota
metadata:
  name: quota-dev
  namespace: rutas-norte-dev
  labels:
    app.kubernetes.io/part-of: rutas-norte
    environment: dev
spec:
  hard:
    requests.cpu: "2"                    # 2 cores committed at most
    requests.memory: 4Gi
    limits.cpu: "4"
    limits.memory: 8Gi
    pods: "25"
    services: "10"
    services.loadbalancers: "0"          # no paid load balancers in dev
    services.nodeports: "2"
    persistentvolumeclaims: "4"
    requests.storage: 10Gi
    configmaps: "30"
    secrets: "30"
    count/deployments.apps: "15"
    count/cronjobs.batch: "5"

rutas-norte-pre

apiVersion: v1
kind: ResourceQuota
metadata:
  name: quota-pre
  namespace: rutas-norte-pre
  labels:
    app.kubernetes.io/part-of: rutas-norte
    environment: pre
spec:
  hard:
    requests.cpu: "4"
    requests.memory: 8Gi
    limits.cpu: "8"
    limits.memory: 12Gi
    pods: "30"
    services: "15"
    services.loadbalancers: "1"          # one, to test the real Ingress
    persistentvolumeclaims: "6"
    requests.storage: 50Gi
    configmaps: "40"
    secrets: "40"
    count/deployments.apps: "20"

rutas-norte-pro

apiVersion: v1
kind: ResourceQuota
metadata:
  name: quota-pro
  namespace: rutas-norte-pro
  labels:
    app.kubernetes.io/part-of: rutas-norte
    environment: pro
spec:
  hard:
    requests.cpu: "24"                   # headroom for the module 9 autoscaling
    requests.memory: 48Gi
    limits.cpu: "48"
    limits.memory: 64Gi
    pods: "150"
    services: "25"
    services.loadbalancers: "3"
    persistentvolumeclaims: "12"
    requests.storage: 500Gi
    configmaps: "60"
    secrets: "60"
    count/deployments.apps: "30"

Comparison and justification:

Resource dev pre pro Why
requests.cpu 2 4 24 pro serves real traffic and must grow on bank holidays
requests.memory 4Gi 8Gi 48Gi Same, plus the cache and the database
pods 25 30 150 The HPA needs a high ceiling in pro
services.loadbalancers 0 1 3 Each one costs money every month
requests.storage 10Gi 50Gi 500Gi The booking data only grows in production
CPU limits/requests ratio Moderate and uniform overcommitment
Memory limits/requests ratio 1.5× 1.33× More conservative the more critical it is

Applying and verifying:

kubectl apply -f k8s/environments/dev/resourcequota.yaml
kubectl apply -f k8s/environments/pre/resourcequota.yaml
kubectl apply -f k8s/environments/pro/resourcequota.yaml
kubectl get resourcequota -A
resourcequota/quota-dev created
resourcequota/quota-pre created
resourcequota/quota-pro created

NAMESPACE         NAME        AGE   REQUEST                                              LIMIT
rutas-norte-dev   quota-dev   8s    pods: 9/25, requests.cpu: 1150m/2, ...               limits.cpu: 2300m/4, ...
rutas-norte-pre   quota-pre   7s    pods: 4/30, requests.cpu: 500m/4, ...                limits.cpu: 1000m/8, ...
rutas-norte-pro   quota-pro   6s    pods: 14/150, requests.cpu: 4200m/24, ...            limits.cpu: 9600m/48, ...

The debt is settled. Now, if somebody scales bookings-api to 50 replicas in development by mistake, or if a restart loop keeps creating pods, the damage stays inside rutas-norte-dev: the pods simply will not be created and production never notices. Remember from module 2 that the namespace isolates neither the network nor DNS; the quota is one of the few things a namespace really does isolate, and that is why it is so valuable.

A final note on the total sum. If you add up the requests.cpu of the three quotas you get 30 cores, and your cluster may have fewer. That is intentional: the quota is a per-environment ceiling, not a reservation. Nobody guarantees you can reach all three ceilings at once; what it guarantees is that no single one can go past its own.

  1. How to choose sensible values

The numbers above do not come from intuition. This is the method.

Step 1: measure, do not guess. You need metrics-server, which we enabled as an addon in module 1:

kubectl top pods -n rutas-norte-pro --sort-by=memory
NAME                                   CPU(cores)   MEMORY(bytes)
bookings-postgres-5d8f6b9c4-k2mnp      180m         1842Mi
bookings-api-7f4b8c9d6-2xkqp           310m         387Mi
bookings-api-7f4b8c9d6-8mvtr           285m         371Mi
redis-cache-6c9d8f7b5-vn4qx            22m          148Mi
notifications-worker-6f7d9c8b4-lm3pt   95m          212Mi
web-store-5b7c9f4d8-h7pnk              8m           14Mi

The detail of this command and its limitations (it is a snapshot, not a history) is the subject of Metrics Server. To calibrate properly you need history with percentiles, which is what Prometheus gives you.

Step 2: apply the formulas.

Resource Formula Reasoning
requests.memory 95th percentile of usage × 1.2 Memory is not throttled: fall short and you die
limits.memory requests.memory × 1.3 to 1.5 Headroom for peaks, without overcommitting
requests.cpu 50th percentile (median) of usage CPU is compressible: the median is enough for the guarantee
limits.cpu requests.cpu × 2 to 4 Absorb bursts without throttling

Note the asymmetry: memory from the 95th percentile, CPU from the median. It is the direct application of section 5.

Step 3: work it out for bookings-api in production. With two weeks of history, including a bank-holiday weekend:

CPU:    median 280m, p95 620m, peak 940m
Memory: median 370Mi, p95 445Mi, peak 502Mi
          resources:
            requests:
              cpu: 300m               # ~ the median
              memory: 534Mi           # 445Mi x 1.2 -> we round to 512Mi
            limits:
              cpu: "1"                # ~ 3x the request, covers the 940m peak
              memory: 768Mi           # 512Mi x 1.5

Step 4: iterate. The initial values are wrong by definition. Review them:

  • After every significant change to the application.
  • After every high season (in Rutas Norte, after each bank holiday and after the summer).
  • Whenever throttling goes above 5% or any OOMKilled appears.
  • Quarterly, to reclaim committed and unused capacity.

Four rules that save you grief:

  1. Start generous and come down. A limit that is too high costs capacity; one that is too low causes outages in production. On the first iteration, prefer the waste.
  2. Multiply by replicas before looking at the quota. requests.cpu: 300m × 8 replicas = 2400m of the pro budget.
  3. Stateful components are different. bookings-postgres must have requests == limits for memory: that is the Guaranteed class from the next lesson, and a database must never be the first candidate to die.
  4. Document where the numbers come from. A comment in the YAML with the measurement date and the percentile used turns the manifest into something reviewable:
          resources:
            # Calibrated 2026-07-28 over 14 days (includes the July bank holiday)
            # CPU: median 280m, p95 620m | Memory: p95 445Mi, peak 502Mi
            requests:
              cpu: 300m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 768Mi

Common Mistakes and Tips

Mistake Symptom Fix
memory: 512m in lower case Immediate CrashLoopBackOff m is milli. Use 512Mi
Confusing M with Mi Unexplained OOMKilled with 6.9% less memory Always use Mi/Gi
Pod Pending with Insufficient memory No node has room for the requests Lower the requests, add a node or autoscale
Looking at actual usage expecting the scheduler to use it "If the node is at 10%, why does it not fit?" The scheduler only looks at requests
Only limits without requests More is reserved than expected Kubernetes copies the limit into the request
No memory limit A single pod can bring down the node Always declare limits.memory
Very tight CPU limit High latency with no restarts or visible errors Check cpu.stat: nr_throttled/nr_periods
JVM or Node unaware of its limit OOMKilled with an apparently sufficient limit MaxRAMPercentage or resourceFieldRef (03-03)
Not checking --previous in the logs "There is nothing in the logs" after a restart kubectl logs --previous
Quota with no LimitRange kubectl run always fails Add a LimitRange (03-05)
Deployment stuck at READY 8/12 The quota rejects the new pods, with no visible error on the Deployment kubectl get events and describe quota
Quota set without telling the team Deployments failing with no explanation Communicate it and leave initial headroom
Not declaring ephemeral-storage Evicted pods and DiskPressure on the node Declare it on anything writing logs or using emptyDir

Tips:

  1. Every manifest template must ship with resources. Put it in your base template so nobody has to remember.
  2. Check the quota before scaling. kubectl describe quota -n <ns> before a kubectl scale.
  3. Watch out for unused requests. It is the biggest invisible cost in a cluster: committed capacity nobody takes advantage of.
  4. Add an alert at 80% of quota. So you find out before a deployment fails, not during.
  5. ephemeral-storage is not optional in production. A node in DiskPressure evicts healthy pods.

Exercises

Exercise 1: Trigger and diagnose both failures

  1. Create in rutas-norte-dev a pod called memory-hog with limits.memory: 128Mi that runs a process trying to allocate 300 MiB. Use the polinux/stress image or busybox with a file in /dev/shm.
  2. Observe its state, identify the Reason and the Exit Code, and explain what 137 means.
  3. Create a pod called cpu-hog with limits.cpu: 100m running an infinite loop.
  4. Check with kubectl top how much CPU it actually consumes and read /sys/fs/cgroup/cpu.stat to work out the throttling percentage.
  5. Explain in a table why one died and the other did not, even though both exceeded their limit.

Exercise 2: Put a quota on the three environments

  1. Apply the three ResourceQuotas from section 12.
  2. Show the current consumption of each namespace with kubectl describe.
  3. Try to scale bookings-api in rutas-norte-dev to a replica count that exceeds the quota. Which command fails and which succeeds? Find the exact error message.
  4. Try to create a pod with kubectl run without declaring resources in rutas-norte-dev. Copy the error and explain it.
  5. Work out how many replicas of bookings-api (with requests: cpu 250m, memory 256Mi) fit at most in rutas-norte-dev according to each of the four compute limits, and say which one governs.

Exercise 3: Calibrate notifications-worker

You have these two weeks of production measurements, including a bank-holiday weekend:

CPU:    median 95m,   p95 340m,  peak 780m
Memory: median 212Mi, p95 268Mi, peak 295Mi
Disk:   logs 400 MiB/day, attachments emptyDir up to 600 MiB
  1. Work out the CPU and memory requests and limits by applying the formulas from section 13, and justify why memory uses the p95 and CPU uses the median.
  2. Decide a value for ephemeral-storage and reason it out.
  3. Write the complete resources block, with the traceability comment.
  4. With 3 replicas in rutas-norte-pro, work out what percentage of the production quota this component consumes.
  5. If the CPU peak is 780m and you set limits.cpu: 700m, what will happen to the component during the bank holiday? Is that acceptable for a mail worker? Compare with the answer if it were bookings-api.

Solutions

Solution 1

apiVersion: v1
kind: Pod
metadata:
  name: memory-hog
  namespace: rutas-norte-dev
  labels:
    app: lab
    environment: dev
spec:
  restartPolicy: Never
  containers:
    - name: stress
      image: polinux/stress:1.0.4
      command: ["stress"]
      args: ["--vm", "1", "--vm-bytes", "300M", "--vm-hang", "1"]
      resources:
        requests:
          cpu: 50m
          memory: 64Mi
        limits:
          cpu: 100m
          memory: 128Mi
kubectl apply -f memory-hog.yaml
sleep 15
kubectl get pod memory-hog -n rutas-norte-dev
kubectl describe pod memory-hog -n rutas-norte-dev | grep -A6 "Last State"
pod/memory-hog created

NAME         READY   STATUS      RESTARTS   AGE
memory-hog   0/1     OOMKilled   0          15s

    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137
      Started:      Wed, 05 Aug 2026 23:41:02 +0200
      Finished:     Wed, 05 Aug 2026 23:41:04 +0200

The 137 is 128 + 9. By Unix convention, a process terminated by a signal returns 128 + signal number, and signal 9 is SIGKILL. It is the unmistakable signature of a violent death: the kernel gave the process no chance to clean up. (Its relative, 143 = 128 + 15, is SIGTERM: graceful termination, the one from module 2.)

The CPU hog:

apiVersion: v1
kind: Pod
metadata:
  name: cpu-hog
  namespace: rutas-norte-dev
  labels:
    app: lab
    environment: dev
spec:
  containers:
    - name: loop
      image: busybox:1.36
      command: ["sh", "-c", "while true; do :; done"]
      resources:
        requests:
          cpu: 50m
          memory: 32Mi
        limits:
          cpu: 100m
          memory: 64Mi
kubectl apply -f cpu-hog.yaml
sleep 60
kubectl top pod cpu-hog -n rutas-norte-dev
kubectl exec cpu-hog -n rutas-norte-dev -- cat /sys/fs/cgroup/cpu.stat
pod/cpu-hog created

NAME      CPU(cores)   MEMORY(bytes)
cpu-hog   100m         1Mi

nr_periods 604
nr_throttled 601
throttled_usec 53420118

The infinite loop would like to consume a whole core (1000m), but kubectl top shows exactly 100m: the limit is enforced to the millicore. And the throttling is 601/604 = 99.5%: practically every period has been cut short. Even so, the pod is Running with no restarts.

memory-hog cpu-hog
Exceeded its limit Yes (300 MiB > 128 MiB) Yes (it wants 1000m > 100m)
Resource Incompressible Compressible
What the kernel could do Nothing: requested memory cannot be "given more slowly" Give it less CPU time
Result OOMKilled, code 137 Running, throttled at 99.5%
Business impact Loss of work in progress Slowness

Solution 2

kubectl apply -f k8s/environments/dev/resourcequota.yaml \
              -f k8s/environments/pre/resourcequota.yaml \
              -f k8s/environments/pro/resourcequota.yaml
kubectl describe quota -n rutas-norte-dev
Name:                     quota-dev
Namespace:                rutas-norte-dev
Resource                  Used    Hard
--------                  ----    ----
count/deployments.apps    5       15
limits.cpu                2300m   4
limits.memory             3200Mi  8Gi
pods                      9       25
requests.cpu              1150m   2
requests.memory           1600Mi  4Gi
services                  4       10
services.loadbalancers    0       0
kubectl scale deploy/bookings-api --replicas=12 -n rutas-norte-dev
kubectl get deploy bookings-api -n rutas-norte-dev
kubectl get events -n rutas-norte-dev --field-selector reason=FailedCreate | tail -2
deployment.apps/bookings-api scaled

NAME           READY   UP-TO-DATE   AVAILABLE   AGE
bookings-api   3/12    12           3           3h

LAST SEEN   TYPE      REASON         OBJECT                             MESSAGE
5s          Warning   FailedCreate   replicaset/bookings-api-6d8f7c9b   Error creating: pods
  "bookings-api-6d8f7c9b-" is forbidden: exceeded quota: quota-dev,
  requested: requests.cpu=250m, used: requests.cpu=1900m, limited: requests.cpu=2

kubectl scale succeeds and the pod creation fails. The Deployment sits at READY 3/12 indefinitely. That asymmetry is the key to the diagnosis: the error is never on the object you touched, but in the ReplicaSet events.

kubectl run test --image=nginx:1.27.1-alpine -n rutas-norte-dev
Error from server (Forbidden): pods "test" is forbidden: failed quota: quota-dev:
  must specify limits.cpu for: test; limits.memory for: test;
  requests.cpu for: test; requests.memory for: test

The quota controls requests.cpu, requests.memory, limits.cpu and limits.memory. To be able to tally those four sums it needs every pod to declare them, so it rejects the one that does not. The elegant fix, without forcing anyone to write them by hand, is the LimitRange from the next lesson.

The maximum replica calculation, with requests: cpu 250m, memory 256Mi and limits: cpu 500m, memory 512Mi:

Quota limit Ceiling Consumption per replica Maximum replicas
requests.cpu 2000m 250m 8
requests.memory 4096Mi 256Mi 16
limits.cpu 4000m 500m 8
limits.memory 8192Mi 512Mi 16
pods 25 1 25

requests.cpu governs (tied with limits.cpu): 8 replicas. The effective limit is always the most restrictive one, and here it is the CPU. And careful: those 8 replicas would be the whole namespace, so you have to subtract what web-store, bookings-postgres, redis-cache and notifications-worker already consume.

Solution 3

  1. Applying the formulas:
requests.memory = p95 x 1.2 = 268Mi x 1.2 = 321.6Mi  -> we round to 320Mi
limits.memory   = 320Mi x 1.5 = 480Mi                -> we round to 512Mi (covers the 295Mi peak comfortably)
requests.cpu    = median = 95m                       -> we round to 100m
limits.cpu      = 100m x 4 = 400m                    -> we raise it to 800m to cover the 780m peak

The asymmetry between memory and CPU is exactly the one from section 5. Memory uses the p95 because falling short does not mean "running slowly", it means OOMKilled: the confirmation email being sent is lost and the customer never gets their ticket. CPU uses the median because falling short only means the worker takes longer to drain the queue; the generous limit lets it speed up and catch up over a bank holiday.

  1. ephemeral-storage: 400 MiB/day of logs plus up to 600 MiB of attachments emptyDir. Assuming daily log rotation and a safety margin:
requests.ephemeral-storage = 400Mi (logs) + 600Mi (emptyDir) = 1Gi
limits.ephemeral-storage   = 1Gi x 2 = 2Gi

The factor of 2 covers the case where log rotation fails for a day or where a bank holiday generates twice as many attachments. Without this limit, a rotation failure would fill the node's disk and cause DiskPressure, evicting the healthy pods sharing the node too, bookings-postgres included.

  1. The complete block:
        - name: worker
          image: registry.rutasnorte.example/notifications-worker:1.8.0
          resources:
            # Calibrated 2026-08-01 over 14 days (includes the 15 August bank holiday)
            # CPU:    median 95m,   p95 340m,  peak 780m  -> request=median, limit>peak
            # Memory: median 212Mi, p95 268Mi, peak 295Mi -> request=p95 x1.2
            # Disk:   400Mi/day of logs + 600Mi of attachments emptyDir
            requests:
              cpu: 100m
              memory: 320Mi
              ephemeral-storage: 1Gi
            limits:
              cpu: 800m
              memory: 512Mi
              ephemeral-storage: 2Gi
  1. With 3 replicas in rutas-norte-pro:
Resource Consumption (3 replicas) pro quota Percentage
requests.cpu 300m 24000m 1.25%
requests.memory 960Mi 49152Mi 1.95%
limits.cpu 2400m 48000m 5.0%
limits.memory 1536Mi 65536Mi 2.3%
pods 3 150 2.0%

A very modest consumption: there is plenty of headroom for bookings-api to scale during the peaks.

  1. With limits.cpu: 700m and a peak of 780m, the worker is throttled during the bank holiday. It does not die: it processes more slowly. The concrete effect is that the notification queue grows and the confirmation emails are delayed, perhaps from 30 seconds to several minutes.

Is that acceptable? For notifications-worker, yes, with caveats. It is an asynchronous process: the customer already has their booking confirmed on screen when the email goes out. A delay of minutes is annoying but does not break the business, and the queue drains itself once the peak passes. You would need to watch that the delay does not grow without bound: if the arrival rate exceeds the processing rate, the queue never recovers and then you really do have an incident.

For bookings-api the answer would be no. It is synchronous: the customer is sitting in front of the screen waiting. Throttling of 20% turns an 80 ms response into a 400 ms one, web-store starts hitting its TIMEOUT_MS of 2000, and the customer sees errors at the very moment of the highest sales of the year. In a user-facing component, the CPU limit must cover the peak comfortably.

That difference — between what merely runs slower and what the customer suffers — is what you must keep in mind when calibrating each component.

Conclusion

You no longer write resources by eye. You understand the whole model: requests are what the scheduler reserves when picking a node and what determines relative CPU priority within the node; limits are the ceiling the kubelet enforces through cgroups at runtime. You know that the scheduler only looks at requests, never at actual usage, which explains why a node at 10% usage can reject a pod. And you have mastered the units, including the two traps that cause real incidents: the 6.9% difference between M and Mi, and the lower-case 512m that sets a limit of half a byte.

You are clear about the fundamental distinction: CPU is compressible and exceeding it only produces throttling — diagnosable with the nr_throttled/nr_periods ratio from cpu.stat, invisible in the pod state — whereas memory is incompressible and exceeding it produces OOMKilled with code 137, with its five typical causes and the reflex of looking at kubectl logs --previous. You know about ephemeral-storage and the disk eviction that can bring down healthy pods across a whole node, and you understand why overcommitting CPU is normal and overcommitting memory is dangerous.

And you have settled the last module 2 debt: the three Rutas Norte environments have a ResourceQuota. rutas-norte-dev cannot go past 2 committed cores and 4 GiB, nor create a single LoadBalancer; rutas-norte-pro has 24 cores and 48 GiB of headroom for bank-holiday and school-holiday peaks. You can read kubectl describe quota, recognise the exceeded-quota error, and diagnose the treacherous case where kubectl scale succeeds and the Deployment sits at READY 8/12 for ever. And you have a method for choosing values: measure with kubectl top, apply the p95 to memory and the median to CPU, document the date and the percentile in the manifest itself, and iterate after every high season.

One loose end remains, and it has come up twice in this lesson. Setting a compute quota forces every pod to declare resources, and that breaks any quick kubectl run and any third-party manifest. The missing piece is the LimitRange, which injects default values into containers that declare nothing. And there is another, even more interesting loose end: we know that when a node runs short of memory somebody dies, but not who. The answer is not random: it depends on a classification Kubernetes assigns to each pod according to how it declared its resources. The next lesson, LimitRanges and Quality of Service (QoS) Classes, covers both things: the default, min, max and maxLimitRequestRatio fields, their exact interaction with the ResourceQuota, and the Guaranteed, Burstable and BestEffort classes with their effect on the kernel's oom_score_adj and on the kubelet's eviction order. By the end you will know why bookings-postgres must be Guaranteed and you will be able to trigger an eviction in your own minikube to see with your own eyes which pod falls first.

Kubernetes Course

Module 1: Introduction to Kubernetes

Module 2: Core Kubernetes Components

Module 3: Configuration and Secret Management

Module 4: Networking in Kubernetes

Module 5: Storage in Kubernetes

Module 6: Advanced Kubernetes Concepts

Module 7: Monitoring and Logging

Module 8: Kubernetes Security

Module 9: Scaling and Performance

Module 10: Kubernetes Ecosystem and Tooling

Module 11: Case Studies and Real-World Applications

Module 12: Preparing for Kubernetes Certification

© Copyright 2026. All rights reserved