All writing

Sizing Go services on Kubernetes: requests, limits and quotas

Kubernetes resource requests, limits, LimitRanges and ResourceQuotas solve different capacity problems. For Go services, those controls must also account for garbage collection, GOMEMLIMIT, container-aware GOMAXPROCS, sidecars, autoscaling and the difference between CPU throttling and memory termination.

about 24 minutes min read

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:

  1. whether the Pod’s use of the starved resource exceeds its request;
  2. Pod priority;
  3. 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:

  1. What resource shape does the workload exhibit under realistic load?
  2. Which CPU and memory values must scheduling plan for?
  3. Which hard limits protect the platform without breaking the SLO?
  4. How do GOMEMLIMIT and GOMAXPROCS relate to those boundaries?
  5. Can rollout surge and maximum autoscaling fit beneath quota?
  6. 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