When multiple tenants share a cluster, each in its own namespace, two namespace-level objects keep one tenant from affecting the others: a `LimitRange` fills in default resource requests and limits and caps container size, and a `ResourceQuota` caps the total resources and object counts the namespace can consume. Both admission controllers are enabled on Syself Autopilot, but they only act on the `LimitRange` and `ResourceQuota` objects you create. Add either to a namespace and it applies to the next pod scheduled there. ## What each tool owns - **LimitRange shapes each pod**: Fills in a default request and limit for any container that omits one, and caps how large a single container may ask to be. It corrects the [request-less pod](/docs/hetzner/apalla/workloads/production/resource-requests-and-limits) for the whole namespace at once, before a scheduler ever sees the pod. - **ResourceQuota fences the total**: Caps what the namespace can consume in aggregate: `requests.cpu`, `requests.memory`, `limits.memory`, and object counts like `pods`, `persistentvolumeclaims`, or `services.loadbalancers`, the last of which stops one tenant from quietly creating a hundred load balancers. Both are namespaced objects you apply once per namespace: ```yaml apiVersion: v1 kind: LimitRange metadata: name: defaults namespace: tenant-acme spec: limits: - type: Container defaultRequest: # request filled in when a container sets none cpu: "250m" memory: "256Mi" default: # limit filled in when a container sets none memory: "256Mi" max: # ceiling a single container may ask for cpu: "2" memory: "2Gi" --- apiVersion: v1 kind: ResourceQuota metadata: name: tenant-acme-quota namespace: tenant-acme spec: hard: requests.cpu: "8" requests.memory: 16Gi limits.memory: 32Gi pods: "50" services.loadbalancers: "2" ``` The quota caps `limits.memory` but leaves `limits.cpu` unset, matching the [per-pod default](/docs/hetzner/apalla/workloads/production/resource-requests-and-limits). Pods here run without a CPU limit so they can use spare cores. CPU is still bounded by `requests.cpu`, which is what the scheduler reserves. The LimitRange above omits a default CPU limit for the same reason. ## Why you need both If you set a quota on `requests.cpu` or `requests.memory`, Kubernetes requires every pod in that namespace to specify those requests. Any pod that omits them is rejected by the API server. The LimitRange keeps this from rejecting pods. It fills in default requests during admission, before the quota is checked, so a pod that omits requests receives the defaults and then passes the quota. Set both, sized to what the tenant pays for, then add [network policy](/docs/hetzner/apalla/security/segment-with-network-policies) and per-namespace RBAC for full isolation. ## Read what a namespace has left Check a namespace for how close it is to reaching its quota with `describe quota`: ```console $ kubectl describe quota tenant-acme-quota -n tenant-acme Name: tenant-acme-quota Namespace: tenant-acme Resource Used Hard -------- ---- ---- pods 12 50 requests.cpu 2500m 8 requests.memory 6Gi 16Gi ``` `Used` against `Hard` is the headroom a namespace has before pods start getting rejected. A deploy that fails with a `must specify` or `exceeded quota` error is the guardrail doing its job. Fix the pod's requests, or raise the ceiling by editing the `hard` values on the ResourceQuota. ## Verify the guardrails Prove both halves on a throwaway namespace. First, that the LimitRange fills in a missing request. Deploy a container with no `resources` and read back what it got: ```console $ kubectl run probe --image=nginx -n tenant-acme $ kubectl get pod probe -n tenant-acme -o jsonpath='{.spec.containers[0].resources.requests}' {"cpu":"250m","memory":"256Mi"} ``` The request you never set is the LimitRange default. Then breach the ceiling on purpose to watch the quota reject it: ```console $ kubectl create deployment flood --image=nginx --replicas=100 -n tenant-acme $ kubectl get events -n tenant-acme | grep -i "exceeded quota" ... Error creating: pods "flood-..." is forbidden: exceeded quota: tenant-acme-quota, requested: pods=1, used: pods=50, limited: pods=50 ``` That rejection is the guardrail working, not a bug. Remove the probe and flood when you are done.