Introduction
Upgrading or migrating Kubernetes LimitRange policies can directly affect whether Pods get scheduled, how much CPU and memory they receive, and whether your namespace stays within guardrails. A change that looks trivial on paper can block new workloads or silently alter resource defaults across an entire team. This guide focuses on Kubernetes LimitRange upgrade and migration with practical examples for developers, DevOps consultants, and technical startup teams. It connects version upgrades, migrations, rollbacks, and validation to concrete commands, expected output, failure signals, and recovery decisions.
The goal is operational safety: observe before changing, limit the blast radius, use placeholders instead of secrets, verify the result, and document how to recover if the expected state is not reached. You will learn how to inventory your current LimitRange configuration, plan a safe migration path, validate changes in a test namespace, diagnose failures, and roll back cleanly when something goes wrong. The examples use Kubernetes 1.26+ and assume kubectl is configured and jq or yq is available for parsing YAML and JSON output.
Version and Environment Inventory
Before touching any LimitRange object, you need a clear picture of what is installed, which Kubernetes version you are running, and which namespaces are subject to the policy. Start with read-only commands that capture the current state without making any changes. This section covers how to gather that inventory and what prerequisites to check.
Check Kubernetes Version and API Availability
LimitRange is a stable API (v1) and has been available since Kubernetes 1.0, but newer fields such as min, max, default, defaultRequest, and maxLimitRequestRatio may behave differently depending on the version. Run:
kubectl version --short
Expected output shows both client and server versions. For example:
Client Version: v1.27.3
Server Version: v1.26.6
If the server version is older than 1.21, some fields like maxLimitRequestRatio may not be supported. Confirm the API is available:
kubectl api-resources | grep limitrange
Expected output:
limitranges limits v1 true LimitRange
List Existing LimitRanges
List all LimitRanges across all namespaces:
kubectl get limitranges --all-namespaces
Example output:
NAMESPACE NAME CREATED AT
default cpu-mem-limit-range 2023-11-01T10:00:00Z
team-a dev-limits 2023-10-15T14:30:00Z
To see the full definition of a specific LimitRange, use:
kubectl get limitrange cpu-mem-limit-range -n default -o yaml
This output includes the spec.limits array with all the rules. Pay attention to the type field: it can be Container, Pod, PersistentVolumeClaim, or EphemeralStorage. Each type has different constraints.
Capture Current State with Timestamps
Before any migration or upgrade, save a snapshot. This is your rollback point and audit record. Use a timestamped directory:
mkdir -p limitrange-backup-$(date +%Y%m%d-%H%M%S)
kubectl get limitranges --all-namespaces -o yaml > limitrange-backup-$(date +%Y%m%d-%H%M%S)/all-limitranges.yaml
Verify the file contains your LimitRange definitions:
head -n 20 limitrange-backup-*/all-limitranges.yaml
Protect Credentials and Private Material
Your LimitRange YAML should not contain secrets, but if you use templating tools like Helm or Kustomize, ensure that values files or patches are not stored in plaintext in public repositories. Use a placeholder pattern in your examples:
apiVersion: v1
kind: LimitRange
metadata:
name: my-limitrange
namespace: my-namespace
spec:
limits:
- default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "200m"
memory: "256Mi"
type: Container
Never commit actual cloud credentials or private keys. If you need to reference a secret-managed value, use a ConfigMap or a secret reference in your GitOps pipeline, not in the LimitRange file itself.
Identify Dependencies: Resource Quota, Namespace, Pod
LimitRanges often work alongside ResourceQuotas. A ResourceQuota sets hard limits on the total resources a namespace can consume, while a LimitRange sets defaults and per-object constraints. They must be compatible: if a LimitRange sets a default request that exceeds the ResourceQuota, Pods may fail to schedule. Check for ResourceQuotas in the same namespace:
kubectl get resourcequota -n my-namespace
Also, consider how existing Pods are affected. If you change a LimitRange, existing Pods are not retroactively modified. Only new Pods are subject to the new defaults and constraints. So, during migration, you must decide whether to restart existing Deployments to pick up default changes.
Practical Kubernetes Checks
kubectl get pods -o wideto see which nodes Pods are on and their status.kubectl describe pod <name>for events related to LimitRange violations (e.g.,Failed to create pod sandbox: pods "x" is forbidden: maximum cpu usage per Container is 1, but limit is 2).kubectl logs <name> --previousto check if a Pod was killed due to resource limit.kubectl rollout status deployment/<name>to confirm a Deployment is fully rolled out after a change.
For a local test, apply one manifest, inspect generated resources, and verify traffic with kubectl port-forward or a service of type NodePort before moving to a cloud load balancer or ingress controller.
Safe Configuration Path
Upgrading or migrating LimitRanges should follow a staged approach: understand the current policy, design the new policy, test in a non-production namespace, then roll out with a rollback plan. This section describes the safe configuration path with concrete examples.
Understand the Current Policy
Let's assume you have an existing LimitRange in the default namespace that provides default CPU and memory requests and limits:
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: default
spec:
limits:
- default:
cpu: "1"
memory: "1Gi"
defaultRequest:
cpu: "500m"
memory: "512Mi"
max:
cpu: "2"
memory: "2Gi"
min:
cpu: "100m"
memory: "128Mi"
type: Container
This policy ensures that every container in the default namespace gets a default request of 500m CPU and 512Mi memory, a default limit of 1 CPU and 1Gi memory, and cannot request more than 2 CPU or 2Gi memory or less than 100m CPU or 128Mi memory.
Now, suppose you want to migrate this policy to a new namespace team-a with different values, or you need to upgrade the policy to add a maxLimitRequestRatio to prevent overcommit.
Design the New Policy
A new policy could look like:
apiVersion: v1
kind: LimitRange
metadata:
name: team-a-limits
namespace: team-a
spec:
limits:
- default:
cpu: "750m"
memory: "768Mi"
defaultRequest:
cpu: "250m"
memory: "256Mi"
max:
cpu: "2"
memory: "2Gi"
min:
cpu: "50m"
memory: "64Mi"
maxLimitRequestRatio:
cpu: "4"
memory: "3"
type: Container
Here, we add maxLimitRequestRatio: the limit-to-request ratio cannot exceed 4 for CPU and 3 for memory. This prevents a container from asking for a tiny request but a huge limit, which helps cluster utilization management.
Test in a Sandbox Namespace
Create a sandbox namespace and apply the new policy:
kubectl create namespace team-a-sandbox
kubectl apply -f team-a-limits.yaml # after changing namespace to team-a-sandbox
Then, create a test Pod without specifying resources to see the defaults applied:
apiVersion: v1
kind: Pod
metadata:
name: test-pod
namespace: team-a-sandbox
spec:
containers:
- name: test-container
image: nginx
Apply it:
kubectl apply -f test-pod.yaml
Check the Pod's effective resources:
kubectl get pod test-pod -n team-a-sandbox -o jsonpath='{.spec.containers[0].resources}'
Expected output should show the default requests and limits as defined:
{"limits":{"cpu":"750m","memory":"768Mi"},"requests":{"cpu":"250m","memory":"256Mi"}}
If the output shows no resources, the LimitRange may not be applied correctly; check if the namespace label matches your LimitRange's metadata.namespace.
Validate Constraints
Try to create a Pod that violates the max CPU:
apiVersion: v1
kind: Pod
metadata:
name: test-pod-violate
namespace: team-a-sandbox
spec:
containers:
- name: test-container
image: nginx
resources:
limits:
cpu: "3"
memory: "1Gi"
requests:
cpu: "1"
memory: "512Mi"
Apply and observe the error:
kubectl apply -f test-pod-violate.yaml
Expected output includes an error like:
Error from server (Forbidden): error when creating "test-pod-violate.yaml": pods "test-pod-violate" is forbidden: maximum cpu usage per Container is 2, but limit is 3.
This confirms the LimitRange is enforced.
Rollout to Production
Once validation passes, apply the policy to the real team-a namespace. But before that, ensure you have a backup of the existing LimitRange in team-a (if any) for rollback. Then apply:
kubectl apply -f team-a-limits.yaml
Monitor the rollout with:
kubectl get limitrange team-a-limits -n team-a -o yaml
Keep Local Test Small
Always test with one manifest, inspect generated resources, and use kubectl port-forward or a NodePort service to verify connectivity before moving to production load balancers. For example, after applying the LimitRange, create a test Deployment and expose it temporarily:
kubectl create deployment test-nginx --image=nginx -n team-a
kubectl expose deployment test-nginx --type=NodePort --port=80 -n team-a
kubectl get svc test-nginx -n team-a
Note the NodePort and test locally with curl or kubectl port-forward:
kubectl port-forward svc/test-nginx 8080:80 -n team-a
Then curl localhost:8080 to verify.
Verification and Diagnostics
After applying a new or updated LimitRange, you need to verify that it is working as intended and diagnose any issues that arise. This section covers verification techniques and common diagnostic commands.
Verify LimitRange Status
Use kubectl describe to see events and any warnings:
kubectl describe limitrange team-a-limits -n team-a
The output should show the rules without errors. For example:
Name: team-a-limits
Namespace: team-a
Type Resource Min Max Default Request Default Limit Max Limit/Request Ratio
---- -------- --- --- --------------- ------------- -----------------------
Container cpu 50m 2 250m 750m 4
Container memory 64Mi 2Gi 256Mi 768Mi 3
If the Max Limit/Request Ratio column shows a value, it is enforced.
Test Policy Enforcement with a Dry-Run
Use kubectl apply --dry-run=client or --dry-run=server to test a Pod without actually creating it. For example:
kubectl apply -f test-pod-violate.yaml --dry-run=server -n team-a
If the Pod would be rejected, you see the same error as above. If it would pass, you see pod/test-pod-violate created (server dry run) (though not actually created).
Check Events for LimitRange Issues
Pods that are rejected due to LimitRange generate events. Use:
kubectl get events -n team-a --sort-by=.metadata.creationTimestamp
Look for FailedCreate or Forbidden messages. For example:
LAST SEEN TYPE REASON OBJECT MESSAGE
3m Warning FailedCreate replicaset/test-rs Error creating: pods "test-rs-abc" is forbidden: maximum memory usage per Container is 2Gi, but limit is 3Gi.
Diagnose Why a Pod Did Not Get Defaults
If a Pod that should have default resources appears without them, check:
- The namespace of the Pod matches the namespace of the LimitRange.
- The LimitRange's
spec.limitsincludes atype: Containerentry (defaults only apply to containers, not Pods or PVCs unless specified). - The Pod spec does not already specify resources; if it does, the LimitRange defaults do not apply, but the constraints still do.
Use kubectl get pod <pod> -n <namespace> -o yaml and inspect spec.containers[].resources. If empty, the LimitRange did not apply.
Check the LimitRange's metadata.namespace; a common mistake is applying the LimitRange to the wrong namespace.
Validate with a JSONPath Query
You can extract the effective resources of a Pod using jsonpath:
kubectl get pod test-pod -n team-a -o jsonpath='{.spec.containers[0].resources}'
Compare with the LimitRange defaults.
Failure Modes and Recovery
No migration is without risk. This section covers common failure modes when upgrading or migrating LimitRanges and how to recover.
Failure Mode 1: Pod Scheduling Failures After Applying New Limits
Symptom: New Pods fail to schedule or are rejected with error messages like forbidden: maximum cpu usage per Container is X, but limit is Y.
Why it happens: The new LimitRange introduced tighter max values, or the existing Pods were created under a more permissive policy and are now being restarted. Existing Pods are not modified, but new Pods from Deployments or StatefulSets will be subject to the new constraints if they don't specify resources or specify values exceeding the new max.
Recovery:
- Identify the failing Pods:
kubectl get pods -n team-a --field-selector=status.phase=Failedorkubectl get eventsto find the error. - Temporarily relax the LimitRange or remove it:
kubectl delete limitrange team-a-limits -n team-a(only if you have a backup; this is a last resort, but it stops the bleeding). - Fix the offending Deployment or Pod spec to comply with the new limits, or adjust the LimitRange to a value that fits your workloads.
- Reapply the intended LimitRange and verify.
Failure Mode 2: Resource Quota Conflict
Symptom: Pods are rejected with a message like exceeded quota: team-a-quota, requested: cpu=750m, used: cpu=3, limited: cpu=4.
Why it happens: The LimitRange defaults (or explicit Pod requests) cause the namespace to exceed the ResourceQuota total.
Recovery:
- Check the ResourceQuota:
kubectl describe resourcequota team-a-quota -n team-a. - Adjust either the ResourceQuota (if allowed) or the LimitRange defaults to lower values.
- Delete or scale down some existing workloads to free resources.
- Ensure that defaults align with the quota: if the quota allows 4 CPUs total, and each Pod default request is 750m, you can only run about 5 Pods. If you need more Pods, lower the default request.
Failure Mode 3: maxLimitRequestRatio Violation
Symptom: Pod creation fails with limit/request ratio for cpu in container test is 5, which exceeds maxLimitRequestRatio of 4.
Why it happens: The Pod specifies a limit-to-request ratio higher than allowed. This often happens when developers set a very low request and a high limit.
Recovery:
- Edit the Pod spec to either lower the limit or raise the request to meet the ratio.
- Or, if the ratio is too strict, adjust the LimitRange
maxLimitRequestRatioto a higher value (or remove it).
Failure Mode 4: Breaking Existing Deployments on Upgrade
Symptom: After upgrading the LimitRange (e.g., increasing min CPU), existing Deployments may fail when rolling out new replicas because their Pod templates now fall below the new min.
Why it happens: The Deployment's Pod template requests less than the new minimum.
Recovery:
- Pause the rollout:
kubectl rollout pause deployment/<name>. - Update the Deployment's resource requests to meet the new min.
- Resume rollout:
kubectl rollout resume deployment/<name>. - If you cannot update immediately, consider temporarily setting the LimitRange min back to the old value until you can update the Deployments.
Rollback Strategy
Always have a rollback plan. The safest rollback is to restore the previous LimitRange from your backup:
kubectl apply -f backup-limitrange.yaml
If you don't have a backup, you can reconstruct it from your Git history or documentation. If you need to remove the LimitRange entirely (which may not be desirable but stops enforcement):
kubectl delete limitrange <name> -n <namespace>
Then, reapply the correct policy.
Operations Checklist
This checklist summarizes the steps for a safe LimitRange upgrade or migration. It is designed to be used as a runbook.
Pre-Upgrade Checklist
- [ ] Confirm Kubernetes version:
kubectl version. - [ ] List existing LimitRanges:
kubectl get limitranges --all-namespaces. - [ ] Backup all LimitRanges:
kubectl get limitranges --all-namespaces -o yaml > limitrange-backup.yaml. - [ ] Identify dependencies: ResourceQuotas, existing Pods, Deployments.
- [ ] Design new LimitRange with clear defaults, max, min, and ratio (if desired).
- [ ] Test new LimitRange in a sandbox namespace.
Upgrade Execution Checklist
- [ ] Apply new LimitRange to target namespace:
kubectl apply -f new-limitrange.yaml. - [ ] Verify with
kubectl describe limitrange. - [ ] Test with a sample Pod to ensure defaults are applied.
- [ ] Test violation cases to ensure constraints are enforced.
- [ ] Monitor events for
ForbiddenorFailedCreatemessages. - [ ] If issues, roll back using backup.
Post-Upgrade Verification Checklist
- [ ] Confirm existing Deployments are still healthy:
kubectl get deployments. - [ ] Check that new Pods get expected defaults:
kubectl get pod <test-pod> -o jsonpath='{.spec.containers[0].resources}'. - [ ] Ensure ResourceQuota is not violated:
kubectl describe resourcequota. - [ ] Document the change in your runbook or Git repository.
Accountable Owner and Review Frequency
Assign a single accountable owner for LimitRange changes, such as the platform team lead or DevOps engineer. This owner is responsible for:
- Approving the new policy.
- Verifying the backup exists.
- Authorizing the rollout.
- Leading the rollback if needed.
Revisit LimitRange policies quarterly or whenever resource usage patterns change significantly. A good practice is to review during the team's regular capacity planning meeting. If you use GitOps, a pull request with at least one reviewer (the owner) should be required.
Common Pitfalls and How to Avoid Them
Many issues with LimitRange upgrades stem from misunderstanding how the policy interacts with other resources. Here are the most common pitfalls and how to avoid or recover from them.
Pitfall 1: Assuming Existing Pods Are Updated
LimitRange changes do not affect existing Pods. If you increase the default request or limit, existing Pods keep their current resources. This can lead to inconsistent behavior within the same namespace.
How to avoid: After changing defaults, explicitly restart Deployments or StatefulSets to apply new defaults to existing workloads, but only after testing. Use kubectl rollout restart deployment/<name> if you want all Pods to be recreated with the new defaults.
Pitfall 2: Ignoring ResourceQuota Interaction
A LimitRange that sets high default requests can quickly exhaust a namespace's ResourceQuota, causing new Pods to fail. Always check the quota before and after.
How to avoid: Use kubectl describe resourcequota and calculate the maximum number of Pods that can be created with the new defaults. Adjust as needed. For example, if quota allows 10 CPUs and default request is 500m, you can run up to 20 Pods (10 / 0.5). If you expect more Pods, lower the default request or increase the quota.
Pitfall 3: Setting Unrealistic Max or Min Values
Too-tight max values can block legitimate workloads; too-loose min values can lead to overcommitment or underutilization. Finding the right balance requires understanding your team's actual usage.
How to avoid: Analyze historical usage metrics (e.g., from Prometheus or kubectl top) before setting limits. Use a range that covers the 95th percentile of observed usage. Revisit quarterly.
Pitfall 4: Forgetting to Include defaultRequest for Burstable Pods
If you only set default (limits) without defaultRequest, Pods that don't specify requests will get requests equal to limits, which may be too high for your quota. Conversely, if you set defaultRequest but not default, Pods may get very low limits, potentially causing OOM kills.
How to avoid: Always set both default and defaultRequest unless you have a specific reason. Test with a sample Pod.
Pitfall 5: Not Testing in a Sandbox
Applying a new LimitRange directly to production can cause an outage. Always test in a sandbox namespace with identical labels and dependencies.
How to avoid: Create a namespace like team-a-staging and apply the same LimitRange. Run a representative workload and verify.
Pitfall 6: Lack of Rollback Plan
If something goes wrong, you need to revert quickly. Without a backup, you may have to reconstruct the LimitRange from memory, causing downtime.
How to avoid: Always backup before changing. Store backups in a version-controlled repository or object storage.
Conclusion
Kubernetes LimitRange upgrades and migrations are safe when approached methodically: inventory the current state, design and test the new policy, validate constraints, and have a rollback plan. The key is to observe before changing and to understand the interactions with ResourceQuotas and existing workloads.
As a next step, choose one low-risk verification for your cluster. For example, create a temporary namespace, apply a new LimitRange with a different default, and create a test Pod to confirm that the defaults are applied and constraints enforced. Record the current state, run the checks, and compare results. Then, incorporate these steps into your team's operational runbook.
A reliable technical workflow makes failure visible, protects sensitive values, limits changes to the intended resource, and defines recovery verification before an incident forces the decision.