Set Kubernetes CPU and Memory Requests and Limits for Django

Size Kubernetes resources for Django web and Celery pods: measure real usage, set memory requests near the working set and limits above peaks, avoid CPU throttling with sensible CPU limits, match Gunicorn workers to CPU requests, and use VPA recommendations.

Problem statement

Your Django pods are either OOM-killed at random, throttled so that latency spikes even with idle nodes, or so over-provisioned that the cluster bill is three times what it should be. Requests and limits drive Kubernetes scheduling, CPU throttling, memory kills, and autoscaling. For Python apps, which use memory per process and are sensitive to CPU throttling, the defaults copied from examples are rarely right.

Requests and limitsMeasure usageCPU, working setRequestsscheduling, HPAMemory limitabove peakCPU limitnone or generousRequests reserve capacity; limits cap it, and wrong caps cause kills or throttling.
  1. Measure actual usage per pod over a week (kubectl top, Prometheus container_memory_working_set_bytes, CPU usage).
  2. Memory: request roughly the steady working set; limit about 1.3 to 1.5 times the observed peak.
  3. CPU: request what the pod uses at normal load; set no CPU limit, or a generous one, to avoid throttling.
  4. Size Gunicorn workers to the CPU request (about 2 to 4 per core for sync workers, fewer with threads).
  5. Use the Vertical Pod Autoscaler in recommendation mode to check your numbers.

Step-by-step solution

1. Measure

kubectl -n prod top pods -l app=web

In Prometheus:

max_over_time(container_memory_working_set_bytes{namespace="prod", container="web"}[7d])
quantile_over_time(0.95, rate(container_cpu_usage_seconds_total{namespace="prod", container="web"}[5m])[7d:5m])

Working set is what the kernel considers in use and what the OOM killer compares against the limit.

2. Set memory requests and limits

resources:
  requests:
    cpu: 500m
    memory: 600Mi
  limits:
    memory: 900Mi

If a pod exceeds its memory limit, the kernel kills it (OOMKilled, exit code 137). For Gunicorn, total memory is roughly the master plus workers times per-worker RSS, and per-worker RSS grows over time; recycle workers with max_requests so the peak is bounded. Requests close to real usage let the scheduler pack nodes without overcommitting memory.

setting and effectEffectGuidanceCPU requestscheduling, HPA basenormal usageCPU limit!throttlingnone or generousMemory requestschedulingsteady working setMemory limit!OOM kill1.3-1.5x peakCPU over the limit is throttled; memory over the limit is killeddjango-deployment.com
HPA CPU targets are percentages of the request, so requests also tune autoscaling.

3. Avoid CPU throttling

CPU limits are enforced in short periods (100 ms by default). A Django request that needs a burst of CPU can be paused mid-request when the pod has used its quota, adding tens or hundreds of milliseconds of latency even when the node is idle. Many teams set CPU requests but no CPU limits for latency-sensitive web pods, relying on requests for fair sharing. If your platform requires limits, set them well above requests (for example 2 to 4 times) and watch container_cpu_cfs_throttled_periods_total.

4. Match Gunicorn to the CPU request

Gunicorn's workers = 2 * cores + 1 formula uses the node's core count inside containers, which is far too many for a pod with a 500m request. Set workers explicitly from the request:

gunicorn config.wsgi --workers 2 --threads 4 --bind 0.0.0.0:8000

More workers mean more memory, so adjust memory requests to match. Celery --concurrency follows the same logic: one CPU-bound task per core, more for I/O-bound tasks.

5. Use VPA recommendations

The Vertical Pod Autoscaler in updateMode: "Off" observes usage and recommends requests without changing pods:

kubectl -n prod describe vpa web

Use its target and upper bound to refine values. Avoid running VPA in auto mode alongside an HPA on CPU for the same Deployment, as they interfere.

measure, set, verify1Measure a week of usageCPU p95, memory peak2Set requests to normal useCPU and memory3Memory limit above peakwith max_requests4Workers from CPU requestnot node cores5Watch OOMs and throttlingadjust monthlyNumbers come from measurement, not from example manifestsdjango-deployment.com
Revisit after major releases; dependencies change memory profiles.

Explanation

The scheduler places pods based on requests, guaranteeing that much capacity. CPU limits are enforced by the kernel's CFS quota, throttling a container that uses more within each period. Memory limits are enforced by the OOM killer, terminating the container. Python web servers use multiple processes with gradually growing memory and short CPU bursts per request, so memory needs headroom and CPU benefits from freedom to burst. Getting these right reduces both incidents and cost.

Edge cases / notes

  • QoS classes. Pods with requests equal to limits for all resources are Guaranteed, evicted last under node pressure; most Django pods are Burstable, which is fine.
  • Startup peaks. Imports and migrations at startup can use more memory than steady state; size limits for that peak or run migrations in a Job.
  • Namespace defaults. LimitRanges may inject default limits you did not intend; check with kubectl describe pod.

Next steps

FAQ

Should Django pods have CPU limits?

Often not, or generous ones. CPU limits cause throttling that adds latency even on idle nodes.

What happens when a pod exceeds its memory limit?

The container is killed with OOMKilled (exit code 137) and restarted.

How many Gunicorn workers per pod?

Base it on the CPU request, not the node: for a 1-core request, 2 to 3 sync workers or fewer workers with threads.

What is VPA recommendation mode?

The Vertical Pod Autoscaler observes usage and suggests requests without changing running pods.

Learning path · step 5 of 16

Django on Kubernetes

Keep going