The basket service starts quickly, responds correctly and appears small on my laptop.
I place it in Kubernetes with no resource configuration:
apiVersion: apps/v1
kind: Deployment
metadata:
name: basket-service
spec:
replicas: 3
selector:
matchLabels:
app: basket-service
template:
metadata:
labels:
app: basket-service
spec:
containers:
- name: basket-service
image: registry.example/basket-service:v1
The manifest is valid.
It is also silent about several important questions:
- How much CPU and memory should scheduling plan for?
- How far may the process burst?
- What happens during throttling or a memory spike?
- How many replicas can the namespace afford?
- What should the autoscaler interpret as high utilisation?
- Do the Go runtime’s CPU and memory controls match the container?
Kubernetes will run the container.
It cannot infer the service’s operating contract from the language used to build the binary.
Resource configuration is the point where application behaviour becomes a scheduling, reliability and cost agreement with the platform.
Throughout this article, bfstore is a hypothetical ecommerce platform. The examples use ordinary container-level resources and assume Go 1.25 or later. They are design examples, not universal production values.
The control map
Kubernetes resource controls answer different questions.
| Control | Primary question | Common failure |
|---|---|---|
| CPU request | How much CPU should scheduling plan for? | Dense packing, contention or wasted capacity |
| Memory request | How much memory should placement account for? | Node pressure and poor eviction behaviour |
| CPU limit | How much CPU throughput may the container consume? | Throttling and latency |
| Memory limit | Where is the hard memory boundary? | OOM termination |
GOMAXPROCS |
How much parallel Go execution should run? | Excess parallelism or underuse |
GOMEMLIMIT |
When should Go increase garbage-collection effort? | GC pressure or cgroup OOM |
LimitRange |
Which individual workload shapes are admitted? | Rejection or misleading defaults |
ResourceQuota |
How much may the namespace claim? | Blocked scaling or rollouts |
These controls cooperate.
They are not interchangeable.
Requests, limits, LimitRanges and ResourceQuotas
Resource requests
A request is a capacity claim used for scheduling:
resources:
requests:
cpu: "250m"
memory: "256Mi"
For ordinary container-level resources, the scheduler accounts for container requests plus the documented init-container and Pod-overhead rules. A Pod is placed only when those requests fit within a node’s allocatable capacity. Kubernetes resource management
Resource limits
A limit is a runtime boundary enforced through operating-system resource controls:
resources:
limits:
cpu: "1"
memory: "512Mi"
CPU and memory limits have different failure semantics.
LimitRange
A LimitRange applies admission policy to individual Pods or containers in one namespace. It can define defaults, minimums, maximums and request-to-limit ratios. Changes do not rewrite existing Pods. Kubernetes LimitRanges
ResourceQuota
A ResourceQuota limits aggregate namespace claims such as total CPU and memory requests or limits, Pod counts and persistent storage. It is admission control, not a namespace CPU throttle, shared memory cgroup or capacity reservation. Namespace quotas can exceed cluster capacity, leaving accepted workloads to contend for placement. Kubernetes ResourceQuotas
CONTAINER
request and limit
│
▼
LIMIT RANGE
is this shape allowed?
│
▼
RESOURCE QUOTA
is namespace budget available?
│
▼
SCHEDULER
can an eligible node place it?
These controls are complementary. A container limit does not define namespace scale, a quota does not rightsize one container, and a default does not prove suitability.
Requests are capacity claims, not private hardware
Suppose a node has:
ALLOCATABLE CPU
4 CPU
It already hosts workloads requesting:
3.6 CPU
A new Pod requests:
500m CPU
The scheduler sees:
3.6 + 0.5 = 4.1 CPU
The Pod does not fit.
This remains true when existing services are idle. The scheduler is protecting the capacity claims represented by their requests.
Requests have different runtime implications for CPU and memory:
| Setting | Scheduling role | Runtime effect |
|---|---|---|
| CPU request | Accounts for planned CPU capacity | Influences relative CPU weight under contention |
| Memory request | Accounts for planned memory capacity | Influences eviction assessment; it is not a private runtime reservation |
| CPU limit | Not the normal-demand estimate | Enforces a throughput ceiling through throttling |
| Memory limit | Not the normal-demand estimate | Defines a cgroup boundary that can lead to OOM termination |
If requests are consistently too high:
- nodes appear full while resources remain idle;
- node autoscaling may add unnecessary capacity;
- deployments remain unplaced;
- namespace quotas are consumed artificially;
- cost rises.
If requests are consistently too low:
- too many Pods can be packed onto a node;
- CPU contention and memory pressure become more likely;
- eviction exposure increases for workloads using far more than requested;
- percentage-based autoscaling signals become misleading.
Requests should describe meaningful ordinary demand, not the smallest values that permit admission.
CPU and memory fail differently
CPU is compressible
A process can receive less CPU time and keep running more slowly.
When a container tries to consume more CPU than its limit permits, the kernel throttles its cgroup. The process is not normally killed merely for demanding additional CPU. Kubernetes resource management
CPU DEMAND ABOVE LIMIT
│
▼
KERNEL THROTTLING
│
▼
WORK CONTINUES MORE SLOWLY
│
▼
LATENCY MAY INCREASE
For a Go HTTP or gRPC service, throttling can delay:
- request handling;
- garbage collection;
- goroutine scheduling;
- TLS and serialisation work;
- health checks;
- graceful shutdown;
- background consumers.
The service may remain alive while failing its latency objective.
Memory is incompressible
Memory cannot always be deferred.
Memory-limit enforcement is reactive. A container can briefly cross the configured value, but when the cgroup cannot satisfy an allocation, the kernel may OOM-kill a process in that cgroup. Kubernetes then observes the container termination. Whether it restarts depends on the Pod’s restart policy and its managing workload. Kubernetes resource management
MEMORY DEMAND
REACHES THE HARD BOUNDARY
│
▼
CGROUP OOM CONDITION
│
▼
PROCESS TERMINATION
│
▼
POSSIBLE CONTAINER RESTART
CPU limits usually produce degraded execution. Memory limits may produce lost execution.
Both can damage availability, but they leave different evidence.
The units deserve respect
Kubernetes CPU is measured in CPU units:
1 CPU = 1000m CPU
Therefore:
250m = 0.25 CPU
500m = 0.5 CPU
1500m = 1.5 CPU
Memory is measured in bytes. I normally use binary units:
256Mi
512Mi
1Gi
The case matters:
memory: "400Mi"
means 400 mebibytes.
memory: "400m"
means 0.4 bytes. Kubernetes calls out this exact class of typo in its resource documentation. Kubernetes resource management
Quoting quantities is not required, but it keeps their intended units visually explicit.
A Go service has two memory boundaries
The Kubernetes memory limit is an external cgroup boundary.
The Go runtime also supports a soft memory limit through:
GOMEMLIMIT
or:
debug.SetMemoryLimit
GOMEMLIMIT includes the Go heap and other memory managed by the Go runtime, including goroutine stacks. It excludes external sources such as the binary’s mappings, memory managed by other languages and memory held by the operating system on the program’s behalf. It is not an RSS or cgroup-memory ceiling. Go runtime environment variables
KUBERNETES MEMORY LIMIT
hard cgroup boundary
a process may be killed
│
▼
HEADROOM
cgo and native allocations
external memory mappings
shared libraries
kernel accounting differences
temporary overshoot and variance
│
▼
GOMEMLIMIT
soft Go runtime target
GC responds more aggressively
Suppose the application container has:
limits:
memory: "512Mi"
An initial hypothesis might be:
env:
- name: GOMEMLIMIT
value: "420MiB" # Illustrative; validate with measurements.
The correct margin depends on the application’s measured memory shape, including:
- Go runtime memory;
- cgo and native libraries;
- memory-mapped files;
- telemetry libraries;
- temporary allocation spikes;
- the difference between runtime and kernel accounting.
A sidecar normally has its own container memory cgroup and does not consume the application container’s 512Mi limit. It still contributes to the Pod’s scheduling total, namespace quota, node pressure and any Pod-level autoscaling metric.
Setting GOMEMLIMIT equal to the container limit leaves little protection against memory outside the runtime’s accounting.
Setting it far too low can drive near-continuous garbage collection. The Go garbage-collector guide describes this as a soft limit: the runtime attempts to respect it while preserving enough CPU for the program to make progress. Go garbage-collector guide
GOMEMLIMIT does not replace the Kubernetes limit.
The Kubernetes limit protects the node and neighbouring workloads.
GOMEMLIMIT gives the Go runtime an earlier signal.
Go 1.25 can use CPU limits for GOMAXPROCS, but not requests
Before Go 1.25, the runtime usually selected GOMAXPROCS from the logical CPUs visible to the process.
A small container on a large node could therefore attempt much more parallel Go execution than its cgroup CPU limit could sustain.
Go 1.25 changed the Linux default. The runtime considers a cgroup CPU bandwidth limit when selecting GOMAXPROCS, periodically checks for changes and rounds fractional CPU limits up to a valid integer. Kubernetes CPU limits normally provide that cgroup input. Kubernetes CPU requests do not. Manually setting GOMAXPROCS disables the automatic default and updates. Go 1.25 release notes Container-aware GOMAXPROCS
KUBERNETES CPU REQUEST
scheduler capacity claim
CPU weight under contention
HPA utilisation denominator
KUBERNETES CPU LIMIT
cgroup throughput ceiling
Go 1.25 runtime input
GOMAXPROCS
parallel Go execution limit
A CPU limit and GOMAXPROCS are not identical.
A CPU limit controls throughput over time.
GOMAXPROCS controls how many threads may execute Go code simultaneously.
The trade-off is therefore explicit:
| Operating model | Benefit | Risk |
|---|---|---|
| CPU request with no CPU limit | Uses otherwise idle node CPU and avoids hard quota throttling | Go cannot derive parallelism from the request; parallelism may reflect the node’s visible CPUs |
| CPU request plus CPU limit | Provides a cgroup ceiling and a Go 1.25 runtime input | Hard throttling can increase latency |
CPU request with an explicit GOMAXPROCS |
Allows burst throughput while constraining Go parallelism | Manual tuning disables automatic updates and must be owned deliberately |
For bfstore, I would not set GOMAXPROCS manually in every Deployment by habit.
I would choose the CPU operating model at platform level, measure throttling and tail latency, and override the runtime only when evidence supports it.
A baseline bfstore Deployment
A baseline should be an explicit, measurable hypothesis.
apiVersion: apps/v1
kind: Deployment
metadata:
name: catalog-service
namespace: bfstore-prod
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: catalog-service
template:
metadata:
labels:
app.kubernetes.io/name: catalog-service
spec:
containers:
- name: catalog-service
image: registry.example/bfstore/catalog-service:v1.0.0
env:
- name: GOMEMLIMIT
value: "420MiB" # Initial hypothesis, not a universal ratio.
ports:
- name: grpc
containerPort: 8080
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
memory: "512Mi"
readinessProbe:
grpc:
port: 8080
periodSeconds: 5
livenessProbe:
grpc:
port: 8080
periodSeconds: 10
This example deliberately omits a CPU limit.
That permits the service to use idle node CPU beyond its request and avoids hard cgroup throttling. It also means Go cannot derive GOMAXPROCS from the 250m request. On a large node, the runtime may select much greater parallelism unless CPU affinity, an explicit runtime value or another effective cgroup boundary narrows it.
A stricter environment might use:
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1"
memory: "512Mi"
That gives the runtime a cgroup CPU limit to consider and constrains prolonged consumption.
It can also create throttling.
Neither design is universally correct.
Sidecars are part of the resource shape
A Pod may contain:
- a service-mesh proxy;
- a telemetry collector;
- a certificate agent;
- a configuration reloader;
- a security agent.
Each container needs its own resource contract:
spec:
containers:
- name: order-service
resources:
requests:
cpu: "300m"
memory: "320Mi"
limits:
memory: "640Mi"
- name: telemetry-sidecar
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
For ordinary container-level resources, the Pod total includes both containers:
CPU REQUEST
300m + 50m = 350m
MEMORY REQUEST
320Mi + 64Mi = 384Mi
MEMORY LIMIT
640Mi + 128Mi = 768Mi
The sidecar also affects autoscaling when the HPA uses aggregate Pod resource utilisation.
That leads to a design decision:
Should scaling follow
the complete Pod cost?
or
Should scaling follow
the application container?
When a stable application container should drive scaling and sidecar usage is incidental, autoscaling/v2 supports a ContainerResource metric:
metrics:
- type: ContainerResource
containerResource:
name: cpu
container: catalog-service
target:
type: Utilization
averageUtilization: 65
Container names then become part of the autoscaling contract. A rename must be coordinated with the HPA. Kubernetes HPA
Requests and limits create a QoS class
Kubernetes assigns each Pod a Quality of Service class.
Guaranteed
Every relevant container has:
- a CPU request;
- an equal CPU limit;
- a memory request;
- an equal memory limit.
Burstable
At least one request or limit exists, but the Pod does not meet all Guaranteed conditions.
BestEffort
No relevant container has a CPU or memory request or limit.
QoS influences node-pressure behaviour, but the common shorthand of “BestEffort, then Burstable, then Guaranteed” is not the complete eviction algorithm.
For node-pressure eviction, Kubernetes considers:
- whether the Pod’s use of the starved resource exceeds its request;
- Pod priority;
- usage relative to the request.
A BestEffort Pod is especially exposed because any positive usage exceeds its zero request. A Guaranteed Pod has stronger protection, but it is not immune to eviction or to a container-level OOM. Kubernetes QoS classes Kubernetes node-pressure eviction Pod priority and eviction ranking
The baseline example is Burstable:
requests:
cpu: "250m"
memory: "256Mi"
limits:
memory: "512Mi"
The gap says:
Plan for this much.
Allow this much memory.
Treat the difference
as deliberate burst space.
Using equal requests and limits to obtain Guaranteed QoS may suit workloads that need predictable placement and stronger pressure protection.
It also accounts the full limit during scheduling.
QoS should follow the workload’s operating requirements, not act as a badge.
The CPU request becomes the HPA denominator
For a percentage-based CPU target, the HorizontalPodAutoscaler compares observed CPU use with the relevant CPU request.
Suppose one Pod consumes:
200m CPU
With a request of:
250m
utilisation is:
200 / 250 = 80%
With a request of:
500m
the same process usage becomes:
200 / 500 = 40%
The application did not change.
The autoscaler’s interpretation did.
If the request is fictional, the target is fictional too.
A Pod-level CPU utilisation metric also depends on the relevant containers having requests. Missing requests can prevent the HPA from calculating utilisation normally. Kubernetes HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: catalog-service
namespace: bfstore-prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: catalog-service
minReplicas: 3
maxReplicas: 12
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 65
behavior:
scaleDown:
stabilizationWindowSeconds: 300
CPU may not be the best demand signal.
Alternatives include:
- request rate per Pod;
- active requests;
- queue depth;
- event-processing lag;
- concurrency;
- latency;
- several metrics together.
autoscaling/v2 can evaluate several metrics and use the highest proposed replica count. Kubernetes HPA
ResourceQuota creates a namespace ceiling
A production namespace may contain:
- application services;
- workers;
- migration Jobs;
- gateways;
- telemetry components;
- old and new replicas during rollouts.
A ResourceQuota can express the namespace’s aggregate ceiling:
apiVersion: v1
kind: ResourceQuota
metadata:
name: bfstore-production-budget
namespace: bfstore-prod
spec:
hard:
requests.cpu: "20"
requests.memory: "40Gi"
limits.cpu: "40"
limits.memory: "80Gi"
pods: "150"
services: "40"
persistentvolumeclaims: "30"
Kubernetes rejects a new or resized object when accepting it would exceed the applicable quota. A Deployment object itself may still be accepted while the controller fails to create all its Pods; the controller status and events expose the admission failure. Kubernetes ResourceQuotas
RESOURCE QUOTA
admission-time ceiling
NOT
namespace CPU throttling
shared memory enforcement
reserved cluster capacity
automatic rightsizing
The budget needs headroom for:
- rollout surge;
- autoscaling;
- Jobs;
- incident tooling;
- operational agents;
- controlled failover.
A quota calculated only from steady-state replicas can block the rollout intended to improve the service.
Quota is capacity governance, not capacity reservation.
LimitRange provides rails, not rightsizing
A LimitRange can reject clearly unsupported shapes and supply defaults:
apiVersion: v1
kind: LimitRange
metadata:
name: bfstore-service-boundaries
namespace: bfstore-prod
spec:
limits:
- type: Container
min:
cpu: "25m"
memory: "32Mi"
max:
cpu: "4"
memory: "4Gi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
default:
cpu: "1"
memory: "512Mi"
This prevents an ordinary container from declaring a shape the namespace or node classes cannot reasonably support.
Defaults still require care.
If ten services silently inherit the same values, the platform has achieved consistency without proving accuracy.
For bfstore:
GOLDEN PATH
explicit reviewed values
LIMIT RANGE
supported individual boundaries
RESOURCE QUOTA
namespace-wide declaration ceiling
The golden path should render an explicit resource block.
The LimitRange should remain a final guardrail for third-party charts, Jobs and incomplete workloads.
Choosing the first numbers
No production service begins with perfect resource data. The first values are hypotheses.
Measure startup, steady-state and post-GC memory; credible memory peaks; background and loaded CPU; goroutine count; cgo or mapping behaviour; and telemetry overhead.
Then test realistic conditions:
- expected and peak traffic;
- large valid payloads and batches;
- slow downstream dependencies, retries and timeouts;
- database contention or Kafka bursts;
- startup and graceful shutdown.
For CPU, observe upper-percentile usage, throttling, GC CPU and latency at saturation. For memory, observe Go runtime memory, working set, RSS, post-GC floor, peaks and OOM events.
Choose requests from the capacity one replica needs during ordinary operation under its SLO, not from its lowest idle measurement.
For memory limits, keep measured headroom above credible peaks while retaining a useful node-protection boundary. For CPU, choose explicitly between a hard ceiling and request-based access to idle capacity.
MEASURE
│
▼
SET AN INITIAL CONTRACT
│
▼
DEPLOY AND OBSERVE
│
▼
ADJUST
kubectl top provides a useful recent snapshot when Metrics Server is available, but durable evidence should come from the observability platform:
- CPU use and throttling;
- working set, RSS, OOM kills and restarts;
- Go runtime memory, GC CPU and goroutines;
- latency, request rate, queue depth or consumer lag;
- desired and admitted replica counts.
A service using little CPU because it is deadlocked is not efficient.
Reading common failures
The workload controller cannot create a Pod
Check ResourceQuota, LimitRange, other admission policies and the Deployment’s ReplicaFailure events. A Deployment can exist while its requested Pods do not.
A Pod exists but remains Pending
Check whether any eligible node can satisfy the requests, then inspect affinity, taints, tolerations, topology constraints and node-class availability. This is a scheduling failure after admission, not the same as a quota rejection.
A container is OOMKilled
Inspect which process was killed, Go runtime memory, cgo or mapping growth, transient allocations and the hard limit. Raising the limit may be correct, but it can also turn a leak into a slower incident.
Latency rises while CPU is throttled
Check the CPU limit, GC bursts, GOMAXPROCS, replica count and downstream retries. The service may be alive while users are already waiting.
HPA scales unexpectedly
Check the credibility of CPU requests, sidecar usage, missing metrics, quota headroom and whether CPU actually represents demand. Consider a ContainerResource metric when the application container should drive scaling.
A node experiences memory pressure
Check whether the Pod exceeds its request, its priority, relative overuse and the node’s reservations and eviction thresholds. QoS is useful context, not the complete eviction algorithm.
Workload profiles are starting ranges
A golden path should reduce blank-page work without pretending to know every service.
Example starting ranges might be:
| Workload shape | Initial CPU request range | Initial memory request range | Primary validation |
|---|---|---|---|
| Small I/O-bound API | 50–150m | 96–192Mi | latency and concurrency |
| Standard request-response API | 150–500m | 192–512Mi | latency, GC and peak memory |
| CPU-governed service | 500m–2 CPU | 256–1024Mi | throttling and tail latency |
| Event worker | evidence-dependent | evidence-dependent | lag, batch size and memory peaks |
The platform should attach governance to the profile:
workload:
profile: standard-go-service
profile_version: "2"
review_after: "30d"
autoscaling:
metric: cpu
target_utilisation: 65
resource_overrides:
requests:
memory: "384Mi"
limits:
memory: "768Mi"
evidence:
load_test: required
owner: catalog-team
The platform can then:
- render explicit Kubernetes values;
- validate them against
LimitRange; - calculate quota and rollout headroom;
- add standard dashboards;
- require evidence for unusually large overrides;
- surface throttling and OOM behaviour after deployment.
A profile is a starting point.
It is not production truth.
The decision checklist
Before approving a resource contract, I now ask:
- What resource shape does the workload exhibit under realistic load?
- Which CPU and memory values must scheduling plan for?
- Which hard limits protect the platform without breaking the SLO?
- How do
GOMEMLIMITandGOMAXPROCSrelate to those boundaries? - Can rollout surge and maximum autoscaling fit beneath quota?
- Which production evidence will trigger rightsizing?
The mental model
GO SERVICE
│
▼
OBSERVED RESOURCE SHAPE
│
┌────────────┴────────────┐
▼ ▼
CPU REQUEST MEMORY REQUEST
scheduling, weight, HPA scheduling, eviction
│ │
└────────────┬────────────┘
▼
┌────────────┴────────────┐
▼ ▼
CPU LIMIT MEMORY LIMIT
throttling OOM wall
│ │
▼ ▼
GOMAXPROCS GOMEMLIMIT
│ │
└────────────┬────────────┘
▼
┌────────────┴────────────┐
▼ ▼
LIMITRANGE RESOURCEQUOTA
individual guardrail namespace ceiling
│
▼
CLUSTER CAPACITY
Requests make placement and autoscaling more honest. Limits define selected runtime boundaries. GOMEMLIMIT gives the garbage collector an earlier signal, while Go 1.25 can align default parallelism with a CPU limit. LimitRange constrains individual shapes, and ResourceQuota constrains the namespace total.
Observability decides whether any of those assumptions were true.
The numbers will change. That is expected.
The failure would be choosing them once, copying them into every service and calling the resulting uniformity a platform.
References and further reading
- Kubernetes resource management for Pods and containers
- Kubernetes ResourceQuotas
- Kubernetes LimitRanges
- Kubernetes Pod Quality of Service classes
- Kubernetes node-pressure eviction
- Kubernetes Pod priority and eviction ranking
- Kubernetes Horizontal Pod Autoscaling
- Go 1.25 release notes
- Container-aware GOMAXPROCS
- Go runtime environment variables
- Go garbage-collector guide