[{"data":1,"prerenderedAt":1040},["ShallowReactive",2],{"post-\u002Fblog\u002Fkubernetes-cpu-memory-requests-limits":3},{"id":4,"title":5,"body":6,"book":1028,"date":1029,"description":1030,"extension":1031,"meta":1032,"navigation":1033,"path":1034,"seo":1035,"stem":1036,"tags":1037,"__hash__":1039},"blog\u002Fblog\u002Fkubernetes-cpu-memory-requests-limits.md","Kubernetes CPU and memory requests are four different knobs",{"type":7,"value":8,"toc":1010},"minimark",[9,13,96,99,102,118,121,126,129,132,135,138,142,145,153,156,162,169,175,178,181,187,194,197,200,203,206,212,215,218,224,227,230,235,238,255,265,268,275,291,294,300,303,307,310,313,324,327,330,333,336,339,343,346,349,352,355,358,361,365,368,371,381,392,396,399,402,405,423,426,437,440,443,482,486,489,500,503,510,514,517,520,523,527,530,533,536,571,574,629,632,643,646,672,675,679,690,693,735,738,744,747,751,757,763,766,769,772,805,812,815,821,824,828,831,878,881,892,910,928,942,946,1006],[10,11,12],"p",{},"For the longest time, when I saw",[14,15,20],"pre",{"className":16,"code":17,"language":18,"meta":19,"style":19},"language-yaml shiki shiki-themes one-light one-dark-pro","resources:\n  requests:\n    cpu: \"100m\"\n    memory: \"256Mi\"\n  limits:\n    cpu: \"400m\"\n    memory: \"1Gi\"\n","yaml","",[21,22,23,36,44,57,68,76,86],"code",{"__ignoreMap":19},[24,25,28,32],"span",{"class":26,"line":27},"line",1,[24,29,31],{"class":30},"sJa8x","resources",[24,33,35],{"class":34},"s5ixo",":\n",[24,37,39,42],{"class":26,"line":38},2,[24,40,41],{"class":30},"  requests",[24,43,35],{"class":34},[24,45,47,50,53],{"class":26,"line":46},3,[24,48,49],{"class":30},"    cpu",[24,51,52],{"class":34},": ",[24,54,56],{"class":55},"sDhpE","\"100m\"\n",[24,58,60,63,65],{"class":26,"line":59},4,[24,61,62],{"class":30},"    memory",[24,64,52],{"class":34},[24,66,67],{"class":55},"\"256Mi\"\n",[24,69,71,74],{"class":26,"line":70},5,[24,72,73],{"class":30},"  limits",[24,75,35],{"class":34},[24,77,79,81,83],{"class":26,"line":78},6,[24,80,49],{"class":30},[24,82,52],{"class":34},[24,84,85],{"class":55},"\"400m\"\n",[24,87,89,91,93],{"class":26,"line":88},7,[24,90,62],{"class":30},[24,92,52],{"class":34},[24,94,95],{"class":55},"\"1Gi\"\n",[10,97,98],{},"I thought request is the minimum and limit is the maximum. CPU has one pair, memory has another, and your pod gets something in between.",[10,100,101],{},"That sounds like common sense, but after a few problems and some digging into these 4 numbers, it turns out they are barely related to each other.",[103,104,105,109,112,115],"ul",{},[106,107,108],"li",{},"CPU request is scheduler math and CPU weight.",[106,110,111],{},"CPU limit is a CFS quota.",[106,113,114],{},"Memory request is scheduler math and OOM preference.",[106,116,117],{},"Memory limit is a cgroup memory ceiling.",[10,119,120],{},"Note: CFS stands for the Completely Fair Scheduler. It is the core algorithm inside the Linux Kernel that decides which process gets to use the CPU. (Google this if you're interested in how it works, quite interesting).",[122,123,125],"h2",{"id":124},"_1-background-context-the-pod-that-looked-fine","1. Background Context: the pod that looked fine",[10,127,128],{},"The workload was a .NET background worker. Every second it woke up, pulled a bunch of data from some hybrid caches, evaluated rules across live matches, built a 30 to 50KB log string, serialized some JSON, pushed work through queue workers, talked to Redis, then went back around the loop. Nothing fancy, but parallel enough to be CPU-spiky.",[10,130,131],{},"Grafana said the pod averaged around 150m CPU. The node had spare CPU. The 400m limit looked generous enough.",[10,133,134],{},"But the logs had gaps. The job logs its start time every second, and sometimes the timestamps jumped by 3 or 4 seconds. Profiling did not show any obvious slow function, Redis looked fine, and the node was not busy.",[10,136,137],{},"Whole ticks were being skipped, so something was clearly wrong with CPU. Here is what the CPU limit and the other 3 numbers actually do.",[122,139,141],{"id":140},"_2-cpu-limit-is-a-tiny-bucket","2. CPU limit is a tiny bucket",[10,143,144],{},"On Linux, Kubernetes usually enforces CPU limits with CFS bandwidth control. In cgroup v1 you see it as:",[14,146,151],{"className":147,"code":149,"language":150,"meta":19},[148],"language-text","cpu.cfs_quota_us\ncpu.cfs_period_us\n","text",[21,152,149],{"__ignoreMap":19},[10,154,155],{},"In cgroup v2 you see it as:",[14,157,160],{"className":158,"code":159,"language":150,"meta":19},[148],"cpu.max\n",[21,161,159],{"__ignoreMap":19},[10,163,164,165,168],{},"The default period is usually 100ms. A ",[21,166,167],{},"400m"," CPU limit means 0.4 CPU:",[14,170,173],{"className":171,"code":172,"language":150,"meta":19},[148],"0.4 * 100ms = 40ms\n",[21,174,172],{"__ignoreMap":19},[10,176,177],{},"So the container gets 40ms of CPU time per 100ms period. Once it spends that budget, the kernel throttles the cgroup until the next period.",[10,179,180],{},"In cgroup v2 that often looks like this:",[14,182,185],{"className":183,"code":184,"language":150,"meta":19},[148],"40000 100000\n",[21,186,184],{"__ignoreMap":19},[10,188,189,190,193],{},"That means 40,000 microseconds of quota in a 100,000 microsecond period. If there is no CPU limit, the first value is ",[21,191,192],{},"max",".",[10,195,196],{},"The quota is shared by all threads in the cgroup. If 8 threads run at once, they can burn the 40ms budget in about 5ms of wall time, and then the whole container waits out the rest of the 100ms period. App threads, timer callbacks, Redis callbacks, GC, everything in that cgroup waits.",[10,198,199],{},"This happens even when the node has idle CPU, because the kernel is only enforcing this cgroup's quota. It is not looking at the rest of the node.",[10,201,202],{},"So a pod averaging 150m can still get throttled by a 400m limit, because the average hides the burst.",[10,204,205],{},"For a worker that wakes up once per second, this shape is common:",[14,207,210],{"className":208,"code":209,"language":150,"meta":19},[148],"900ms mostly idle\n100ms busy with several threads\n",[21,211,209],{"__ignoreMap":19},[10,213,214],{},"The 1 minute average looks boring, but the problem happens inside the 100ms quota window.",[10,216,217],{},"A burst that needs 200ms of actual CPU under a 400m limit needs five quota periods:",[14,219,222],{"className":220,"code":221,"language":150,"meta":19},[148],"40ms quota per period * 5 periods = 200ms CPU\n5 periods * 100ms = 500ms wall time\n",[21,223,221],{"__ignoreMap":19},[10,225,226],{},"Add a Gen2 GC or a Redis callback backlog in the same second and the log gaps make sense.",[10,228,229],{},"This is also why it's so confusing to debug. The app isn't slow in the normal sense, and there may be no bad LINQ query, lock, or slow network call. The process just stops getting CPU for chunks of time.",[231,232,234],"h3",{"id":233},"the-metric-that-tells-on-it","The metric that tells on it",[10,236,237],{},"If you have Prometheus metrics from cAdvisor, these are the two I usually check:",[14,239,243],{"className":240,"code":241,"language":242,"meta":19,"style":19},"language-promql shiki shiki-themes one-light one-dark-pro","rate(container_cpu_cfs_throttled_periods_total[5m])\nrate(container_cpu_cfs_throttled_seconds_total[5m])\n","promql",[21,244,245,250],{"__ignoreMap":19},[24,246,247],{"class":26,"line":27},[24,248,249],{},"rate(container_cpu_cfs_throttled_periods_total[5m])\n",[24,251,252],{"class":26,"line":38},[24,253,254],{},"rate(container_cpu_cfs_throttled_seconds_total[5m])\n",[10,256,257,260,261,264],{},[21,258,259],{},"throttled_periods"," tells you how often the cgroup hit quota. ",[21,262,263],{},"throttled_seconds"," tells you how much wall time got spent waiting.",[10,266,267],{},"A small amount of throttling is not always a problem. Batch jobs can tolerate it. A web API or 1 second worker may not. I care more when app latency, queue delay, or tick gaps line up with throttled seconds.",[10,269,270,271,274],{},"Inside a container on cgroup v2, ",[21,272,273],{},"cpu.stat"," is also useful:",[14,276,280],{"className":277,"code":278,"language":279,"meta":19,"style":19},"language-bash shiki shiki-themes one-light one-dark-pro","cat \u002Fsys\u002Ffs\u002Fcgroup\u002Fcpu.stat\n","bash",[21,281,282],{"__ignoreMap":19},[24,283,284,288],{"class":26,"line":27},[24,285,287],{"class":286},"sAdtL","cat",[24,289,290],{"class":55}," \u002Fsys\u002Ffs\u002Fcgroup\u002Fcpu.stat\n",[10,292,293],{},"You may see fields like:",[14,295,298],{"className":296,"code":297,"language":150,"meta":19},[148],"nr_periods 12345\nnr_throttled 678\nthrottled_usec 9012345\n",[21,299,297],{"__ignoreMap":19},[10,301,302],{},"It's a good sanity check when the dashboards average everything out.",[122,304,306],{"id":305},"_3-cpu-request-is-not-a-smaller-cpu-limit","3. CPU request is not a smaller CPU limit",[10,308,309],{},"CPU request does not cap the pod, so you can use more than you request. It does two other jobs.",[10,311,312],{},"First, the scheduler uses it for placement. If a pod requests 500m, Kubernetes puts it on a node where the sum of CPU requests still fits. On that node, each pod is guaranteed at least its request.",[10,314,315,316,319,320,323],{},"Second, the runtime turns it into CPU weight. In cgroup v1 this was ",[21,317,318],{},"cpu.shares",". In cgroup v2 it is ",[21,321,322],{},"cpu.weight",". When the node is busy, that weight decides how CPU time is split between cgroups.",[10,325,326],{},"On an idle node, a pod requesting 100m can use 4 cores if nothing stops it. That isn't stealing, it's how CPU is supposed to work: spare CPU is available until somebody else needs it.",[10,328,329],{},"Say 2 pods request 500m and 1000m on a node with 3000m CPU. If both try to use as much CPU as they can and nothing else on the node competes, they get up to 1000m and 2000m, split by their request weights.",[10,331,332],{},"That's why removing CPU limits is usually fine for normal services. When CPU is contested, other pods are still protected by their requests. Your pod can use idle cycles, but it can't ignore everyone else's weight on a busy node.",[10,334,335],{},"You trade \"hard pause every time I hit quota\" for \"normal Linux scheduling under contention\", and for bursty workloads I prefer the second.",[10,337,338],{},"One caveat: in shared or multi-tenant clusters, some teams keep CPU limits on purpose as a cost or fair-use control, so one team's burst does not eat another team's headroom. In that case, removing the limit is a conversation to have with whoever owns cluster capacity.",[231,340,342],{"id":341},"the-hpa-gotcha","The HPA gotcha",[10,344,345],{},"CPU request is also the denominator for HPA CPU utilization.",[10,347,348],{},"If a pod requests 100m and uses 150m, HPA sees 150% CPU utilization for that pod. If the same pod requests 500m and uses 150m, HPA sees 30%.",[10,350,351],{},"The workload and CPU usage are the same, but the scaling signal is different.",[10,353,354],{},"So if HPA scales on CPU utilization, raising the request is not a free change. You may need to adjust the HPA target at the same time, or switch to a raw CPU metric or an app metric.",[10,356,357],{},"Also, if CPU request is missing for a container, HPA cannot calculate CPU utilization for that pod. This is another reason BestEffort pods and CPU-based HPA do not mix well.",[10,359,360],{},"Requests look like capacity planning, so it's easy to forget they affect autoscaling too.",[122,362,364],{"id":363},"_4-memory-limit-is-not-throttle-it-kills","4. Memory limit is not throttle, it kills",[10,366,367],{},"Memory does not fail as nicely as CPU. CPU is compressible: the kernel can give you less CPU time and your app still runs, just slower.",[10,369,370],{},"Memory isn't compressible. Once memory is allocated, the kernel cannot ask your process to \"use RAM slower\". It can reclaim page cache and apply pressure, and eventually it kills something.",[10,372,373,374,377,378,193],{},"In cgroup v1, the memory limit maps to ",[21,375,376],{},"memory.limit_in_bytes",". In cgroup v2, it maps to ",[21,379,380],{},"memory.max",[10,382,383,384,387,388,391],{},"If your container crosses the memory limit and the kernel cannot reclaim enough memory, the OOM killer gets involved. In the usual one-process container, PID 1 dies and Kubernetes restarts the container. Your app does not get a clean exception and ",[21,385,386],{},"finally"," blocks do not run. The pod status says ",[21,389,390],{},"OOMKilled"," and that's it.",[122,393,395],{"id":394},"_5-memory-request-is-not-reserved-ram","5. Memory request is not reserved RAM",[10,397,398],{},"Memory request is mainly scheduler math.",[10,400,401],{},"If a pod requests 1Gi, Kubernetes only places it on a node with 1Gi of unrequested memory. The kernel does not set that 1Gi aside for the pod or stop anyone else from using it.",[10,403,404],{},"At runtime, the request matters when the node is under memory pressure. Kubernetes uses the QoS class and OOM score adjustment to decide which pods to evict or kill first.",[10,406,407,408,411,412,415,416,419,420,422],{},"On plain cgroup setups, memory request does not normally become ",[21,409,410],{},"memory.min"," or ",[21,413,414],{},"memory.low",". Kubernetes has a MemoryQoS feature for cgroup v2 that adds more memory protection, but don't assume it's on unless you know your cluster enabled it. MemoryQoS also uses ",[21,417,418],{},"memory.high",", a soft ceiling where the kernel starts slowing down allocation before the cgroup reaches ",[21,421,380],{},". That's the closest thing memory has to CPU throttling, though it's still not the same guarantee.",[10,424,425],{},"The rough idea:",[103,427,428,431,434],{},[106,429,430],{},"Guaranteed pods are protected the most.",[106,432,433],{},"BestEffort pods are the easiest to evict or kill.",[106,435,436],{},"Burstable pods sit in the middle, and using much more than requested does not help your case.",[10,438,439],{},"So a tiny memory request is not only a scheduling lie, it also moves the pod closer to the front of the kill line when the node runs out of memory.",[10,441,442],{},"For services I care about, I set the memory request close to the normal working set and the memory limit at the point where a restart is better than letting the process keep growing. Often that means request equals limit:",[14,444,446],{"className":16,"code":445,"language":18,"meta":19,"style":19},"resources:\n  requests:\n    memory: \"1Gi\"\n  limits:\n    memory: \"1Gi\"\n",[21,447,448,454,460,468,474],{"__ignoreMap":19},[24,449,450,452],{"class":26,"line":27},[24,451,31],{"class":30},[24,453,35],{"class":34},[24,455,456,458],{"class":26,"line":38},[24,457,41],{"class":30},[24,459,35],{"class":34},[24,461,462,464,466],{"class":26,"line":46},[24,463,62],{"class":30},[24,465,52],{"class":34},[24,467,95],{"class":55},[24,469,470,472],{"class":26,"line":59},[24,471,73],{"class":30},[24,473,35],{"class":34},[24,475,476,478,480],{"class":26,"line":70},[24,477,62],{"class":30},[24,479,52],{"class":34},[24,481,95],{"class":55},[122,483,485],{"id":484},"_6-qos-has-a-trap","6. QoS has a trap",[10,487,488],{},"Kubernetes QoS is like a survival ranking:",[103,490,491,494,497],{},[106,492,493],{},"Guaranteed: every container has CPU and memory request and limit set, and request equals limit for both.",[106,495,496],{},"Burstable: at least one request or limit is set, but the pod is not Guaranteed.",[106,498,499],{},"BestEffort: no requests and no limits.",[10,501,502],{},"If you remove the CPU limit, the pod is no longer Guaranteed, even if memory request equals memory limit.",[10,504,505,506,509],{},"That can be fine. I'd rather run a normal API pod with no CPU limit than force ",[21,507,508],{},"cpu request == cpu limit"," to get Guaranteed QoS and then spend the next month chasing latency spikes from throttling.",[122,511,513],{"id":512},"_7-guaranteed-qos-and-cpu-manager-static-policy","7. Guaranteed QoS and CPU Manager static policy",[10,515,516],{},"CPU Manager static policy is a kubelet feature that can give a container exclusive physical CPU cores instead of time-slicing it with everything else on the node. It only pins cores when the pod is Guaranteed QoS and the CPU request is a whole integer that matches the limit (e.g., 1, 2, or 4, not 1.5 or 150m).",[10,518,519],{},"Exclusive cores mean no noisy-neighbor interference and no context-switching jitter. That's useful for low-latency, CPU-sensitive workloads like real-time signal processing, high-frequency trading, telco\u002FNFV, or anything sensitive to CPU cache eviction and scheduling jitter.",[10,521,522],{},"It's not meant for typical workloads with variable usage patterns, like a service that averages 150 millicpu but occasionally spikes to 400m. For that kind of workload, Burstable QoS with a modest CPU request and no CPU limit is the better fit, like the default in section 11.",[122,524,526],{"id":525},"_8-the-defaults-can-betray-you","8. The defaults can betray you",[10,528,529],{},"Two Kubernetes defaults can change what you asked for.",[10,531,532],{},"First, if you set a limit but no request, Kubernetes may copy the limit into the request, unless an admission policy already supplied a request.",[10,534,535],{},"This:",[14,537,539],{"className":16,"code":538,"language":18,"meta":19,"style":19},"resources:\n  limits:\n    cpu: \"500m\"\n    memory: \"512Mi\"\n",[21,540,541,547,553,562],{"__ignoreMap":19},[24,542,543,545],{"class":26,"line":27},[24,544,31],{"class":30},[24,546,35],{"class":34},[24,548,549,551],{"class":26,"line":38},[24,550,73],{"class":30},[24,552,35],{"class":34},[24,554,555,557,559],{"class":26,"line":46},[24,556,49],{"class":30},[24,558,52],{"class":34},[24,560,561],{"class":55},"\"500m\"\n",[24,563,564,566,568],{"class":26,"line":59},[24,565,62],{"class":30},[24,567,52],{"class":34},[24,569,570],{"class":55},"\"512Mi\"\n",[10,572,573],{},"can effectively become this:",[14,575,577],{"className":16,"code":576,"language":18,"meta":19,"style":19},"resources:\n  requests:\n    cpu: \"500m\"\n    memory: \"512Mi\"\n  limits:\n    cpu: \"500m\"\n    memory: \"512Mi\"\n",[21,578,579,585,591,599,607,613,621],{"__ignoreMap":19},[24,580,581,583],{"class":26,"line":27},[24,582,31],{"class":30},[24,584,35],{"class":34},[24,586,587,589],{"class":26,"line":38},[24,588,41],{"class":30},[24,590,35],{"class":34},[24,592,593,595,597],{"class":26,"line":46},[24,594,49],{"class":30},[24,596,52],{"class":34},[24,598,561],{"class":55},[24,600,601,603,605],{"class":26,"line":59},[24,602,62],{"class":30},[24,604,52],{"class":34},[24,606,570],{"class":55},[24,608,609,611],{"class":26,"line":70},[24,610,73],{"class":30},[24,612,35],{"class":34},[24,614,615,617,619],{"class":26,"line":78},[24,616,49],{"class":30},[24,618,52],{"class":34},[24,620,561],{"class":55},[24,622,623,625,627],{"class":26,"line":88},[24,624,62],{"class":30},[24,626,52],{"class":34},[24,628,570],{"class":55},[10,630,631],{},"That may be fine, but it is not always what you meant.",[10,633,634,635,638,639,642],{},"Second, a ",[21,636,637],{},"LimitRange"," can inject default requests and limits into pods at admission time. You can remove ",[21,640,641],{},"limits.cpu"," from your manifest and still get a CPU limit after the API server admits the pod.",[10,644,645],{},"If throttling makes no sense, check the live pod:",[14,647,649],{"className":277,"code":648,"language":279,"meta":19,"style":19},"kubectl get pod my-pod -o yaml\n",[21,650,651],{"__ignoreMap":19},[24,652,653,656,659,662,665,669],{"class":26,"line":27},[24,654,655],{"class":286},"kubectl",[24,657,658],{"class":55}," get",[24,660,661],{"class":55}," pod",[24,663,664],{"class":55}," my-pod",[24,666,668],{"class":667},"sAGMh"," -o",[24,670,671],{"class":55}," yaml\n",[10,673,674],{},"Do not only check the Helm values or the Deployment template you thought you applied.",[122,676,678],{"id":677},"_9-the-tmpfs-emptydir-surprise","9. The tmpfs emptyDir surprise",[10,680,681,682,685,686,689],{},"An ",[21,683,684],{},"emptyDir"," with ",[21,687,688],{},"medium: Memory"," counts against your memory limit.",[10,691,692],{},"For example:",[14,694,696],{"className":16,"code":695,"language":18,"meta":19,"style":19},"volumes:\n  - name: scratch\n    emptyDir:\n      medium: Memory\n",[21,697,698,705,718,725],{"__ignoreMap":19},[24,699,700,703],{"class":26,"line":27},[24,701,702],{"class":30},"volumes",[24,704,35],{"class":34},[24,706,707,710,713,715],{"class":26,"line":38},[24,708,709],{"class":34},"  - ",[24,711,712],{"class":30},"name",[24,714,52],{"class":34},[24,716,717],{"class":55},"scratch\n",[24,719,720,723],{"class":26,"line":46},[24,721,722],{"class":30},"    emptyDir",[24,724,35],{"class":34},[24,726,727,730,732],{"class":26,"line":59},[24,728,729],{"class":30},"      medium",[24,731,52],{"class":34},[24,733,734],{"class":55},"Memory\n",[10,736,737],{},"Files written there live in tmpfs, outside your language runtime, so the .NET GC cannot see or clean them. If your app writes 600Mi of temporary files there, that 600Mi still counts.",[10,739,740,741,743],{},"Per the Kubernetes docs, if you set a memory limit, a memory-backed ",[21,742,684],{}," can grow up to the pod's memory limit unless you give it its own size limit.",[10,745,746],{},"So if a pod OOMs and heap graphs look fine, check tmpfs, file buffers, native memory, and page cache. Not every OOM is a managed heap problem.",[122,748,750],{"id":749},"_10-the-net-gotcha-after-removing-cpu-limit","10. The .NET gotcha after removing CPU limit",[10,752,753,756],{},[21,754,755],{},"Environment.ProcessorCount"," respects CPU utilization limits. With a 400m CPU limit, .NET rounds up and the process usually sees:",[14,758,761],{"className":759,"code":760,"language":150,"meta":19},[148],"ProcessorCount = 1\n",[21,762,760],{"__ignoreMap":19},[10,764,765],{},"Remove the CPU limit and the runtime may suddenly see the node's CPU count. On a 32 core node, that can change ThreadPool behavior, parallelism defaults, and Server GC heap count.",[10,767,768],{},"So the CPU fix can turn into a memory change. You remove the CPU limit, throttling goes away, and then the pod starts using more memory because the runtime now thinks it's on a much bigger machine. That's a fun one to explain.",[10,770,771],{},"If the app depends on runtime CPU count, pin it explicitly:",[14,773,775],{"className":16,"code":774,"language":18,"meta":19,"style":19},"env:\n  - name: DOTNET_PROCESSOR_COUNT\n    value: \"2\"\n",[21,776,777,784,795],{"__ignoreMap":19},[24,778,779,782],{"class":26,"line":27},[24,780,781],{"class":30},"env",[24,783,35],{"class":34},[24,785,786,788,790,792],{"class":26,"line":38},[24,787,709],{"class":34},[24,789,712],{"class":30},[24,791,52],{"class":34},[24,793,794],{"class":55},"DOTNET_PROCESSOR_COUNT\n",[24,796,797,800,802],{"class":26,"line":46},[24,798,799],{"class":30},"    value",[24,801,52],{"class":34},[24,803,804],{"class":55},"\"2\"\n",[10,806,807,808,811],{},"Do not copy ",[21,809,810],{},"4"," because a blog post used it. Pick the number based on how much parallelism the app should have.",[10,813,814],{},"For JVM workloads, the equivalent knob is usually:",[14,816,819],{"className":817,"code":818,"language":150,"meta":19},[148],"-XX:ActiveProcessorCount=4\n",[21,820,818],{"__ignoreMap":19},[10,822,823],{},"Either way, set the runtime's view of the machine on purpose instead of letting it guess.",[122,825,827],{"id":826},"_11-my-default-now","11. My default now",[10,829,830],{},"For most normal services, I like this shape:",[14,832,834],{"className":16,"code":833,"language":18,"meta":19,"style":19},"resources:\n  requests:\n    cpu: \"500m\"\n    memory: \"1Gi\"\n  limits:\n    memory: \"1Gi\"\n",[21,835,836,842,848,856,864,870],{"__ignoreMap":19},[24,837,838,840],{"class":26,"line":27},[24,839,31],{"class":30},[24,841,35],{"class":34},[24,843,844,846],{"class":26,"line":38},[24,845,41],{"class":30},[24,847,35],{"class":34},[24,849,850,852,854],{"class":26,"line":46},[24,851,49],{"class":30},[24,853,52],{"class":34},[24,855,561],{"class":55},[24,857,858,860,862],{"class":26,"line":59},[24,859,62],{"class":30},[24,861,52],{"class":34},[24,863,95],{"class":55},[24,865,866,868],{"class":26,"line":70},[24,867,73],{"class":30},[24,869,35],{"class":34},[24,871,872,874,876],{"class":26,"line":78},[24,873,62],{"class":30},[24,875,52],{"class":34},[24,877,95],{"class":55},[10,879,880],{},"Then tune from real metrics.",[10,882,883,884,887,888,891],{},"The Vertical Pod Autoscaler helps with the tuning. Set its ",[21,885,886],{},"updateMode"," to ",[21,889,890],{},"Off"," and VPA computes recommendations from historical usage and shows them in the VerticalPodAutoscaler object's status, but never applies them, so there are no restarts or evictions.",[14,893,895],{"className":277,"code":894,"language":279,"meta":19,"style":19},"kubectl describe vpa my-app-vpa\n",[21,896,897],{"__ignoreMap":19},[24,898,899,901,904,907],{"class":26,"line":27},[24,900,655],{"class":286},[24,902,903],{"class":55}," describe",[24,905,906],{"class":55}," vpa",[24,908,909],{"class":55}," my-app-vpa\n",[10,911,912,913,916,917,916,920,923,924,927],{},"That gives you ",[21,914,915],{},"target",", ",[21,918,919],{},"lowerBound",[21,921,922],{},"upperBound",", and ",[21,925,926],{},"uncappedTarget"," for CPU and memory, based on what the pod actually used, not a guess.",[10,929,930,931,933,934,937,938,941],{},"VPA's recommendation is based on usage, not throttling. It will tell you p90 CPU usage looked fine, but not that the cgroup got throttled 40 times last week. That is the same gap this post started with. So I use VPA in ",[21,932,890],{}," mode as an input for picking a request, and I still check ",[21,935,936],{},"cfs_throttled_periods"," and ",[21,939,940],{},"cfs_throttled_seconds"," from section 2 before I trust the number.",[122,943,945],{"id":944},"references","References",[103,947,948,957,964,971,978,985,992,999],{},[106,949,950],{},[951,952,956],"a",{"href":953,"rel":954},"https:\u002F\u002Fkubernetes.io\u002Fdocs\u002Fconcepts\u002Fconfiguration\u002Fmanage-resources-containers\u002F",[955],"nofollow","Kubernetes resource management for Pods and containers",[106,958,959],{},[951,960,963],{"href":961,"rel":962},"https:\u002F\u002Fkubernetes.io\u002Fdocs\u002Ftasks\u002Fconfigure-pod-container\u002Fquality-service-pod\u002F",[955],"Kubernetes QoS classes",[106,965,966],{},[951,967,970],{"href":968,"rel":969},"https:\u002F\u002Fkubernetes.io\u002Fdocs\u002Ftasks\u002Fadminister-cluster\u002Fcpu-management-policies\u002F",[955],"Kubernetes CPU Manager policies",[106,972,973],{},[951,974,977],{"href":975,"rel":976},"https:\u002F\u002Fkubernetes.io\u002Fdocs\u002Fconcepts\u002Fworkloads\u002Fautoscaling\u002Fhorizontal-pod-autoscale\u002F",[955],"Kubernetes Horizontal Pod Autoscaling",[106,979,980],{},[951,981,984],{"href":982,"rel":983},"https:\u002F\u002Fkubernetes.io\u002Fdocs\u002Fconcepts\u002Fpolicy\u002Flimit-range\u002F",[955],"Kubernetes LimitRange",[106,986,987],{},[951,988,991],{"href":989,"rel":990},"https:\u002F\u002Fgithub.com\u002Fkubernetes\u002Fautoscaler\u002Ftree\u002Fmaster\u002Fvertical-pod-autoscaler",[955],"Vertical Pod Autoscaler",[106,993,994],{},[951,995,998],{"href":996,"rel":997},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fdotnet\u002Fapi\u002Fsystem.environment.processorcount",[955],".NET Environment.ProcessorCount",[106,1000,1001],{},[951,1002,1005],{"href":1003,"rel":1004},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fdotnet\u002Fcore\u002Fcompatibility\u002Fcore-libraries\u002F6.0\u002Fenvironment-processorcount-on-windows",[955],".NET DOTNET_PROCESSOR_COUNT override",[1007,1008,1009],"style",{},"html pre.shiki code .sJa8x, html code.shiki .sJa8x{--shiki-default:#E45649;--shiki-dark:#E06C75}html pre.shiki code .s5ixo, html code.shiki .s5ixo{--shiki-default:#383A42;--shiki-dark:#ABB2BF}html pre.shiki code .sDhpE, html code.shiki .sDhpE{--shiki-default:#50A14F;--shiki-dark:#98C379}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sAdtL, html code.shiki .sAdtL{--shiki-default:#4078F2;--shiki-dark:#61AFEF}html pre.shiki code .sAGMh, html code.shiki .sAGMh{--shiki-default:#986801;--shiki-dark:#D19A66}",{"title":19,"searchDepth":38,"depth":38,"links":1011},[1012,1013,1016,1019,1020,1021,1022,1023,1024,1025,1026,1027],{"id":124,"depth":38,"text":125},{"id":140,"depth":38,"text":141,"children":1014},[1015],{"id":233,"depth":46,"text":234},{"id":305,"depth":38,"text":306,"children":1017},[1018],{"id":341,"depth":46,"text":342},{"id":363,"depth":38,"text":364},{"id":394,"depth":38,"text":395},{"id":484,"depth":38,"text":485},{"id":512,"depth":38,"text":513},{"id":525,"depth":38,"text":526},{"id":677,"depth":38,"text":678},{"id":749,"depth":38,"text":750},{"id":826,"depth":38,"text":827},{"id":944,"depth":38,"text":945},null,"2026-07-08","How CPU and memory requests and limits map to different scheduler, cgroup, and OOM behavior.","md",{},true,"\u002Fblog\u002Fkubernetes-cpu-memory-requests-limits",{"title":5,"description":1030},"blog\u002Fkubernetes-cpu-memory-requests-limits",[1038],"tech","F-o6p4kRNN1KGloK6-n15Vc5h6ZJ7H2ESMehzSktf4E",1792143571343]