Autoscale Django on Kubernetes With the HPA
Autoscale Django web pods on Kubernetes with the Horizontal Pod Autoscaler: accurate CPU requests, scaling on CPU or requests per second, min and max replicas, scale-down stabilisation, database connection limits, and testing under load.
Problem statement
A fixed number of Django pods is either too many at night or too few during a marketing campaign. The Horizontal Pod Autoscaler (HPA) adds and removes replicas based on metrics, but it only works well when pods have realistic resource requests, the chosen metric actually tracks load, and downstream systems (especially PostgreSQL connections) can handle the maximum replica count. Misconfigured, it flaps up and down, scales too late, or overwhelms the database.
- Set realistic CPU and memory
requestson Django containers, measured under load. - Start with CPU-based scaling:
averageUtilization: 60-70withminReplicasof at least 2. - For I/O-bound apps, scale on requests per second per pod via Prometheus Adapter or KEDA instead of CPU.
- Cap
maxReplicasso total database connections (replicas × workers × threads) stay under the limit, or add PgBouncer. - Use
behaviorto scale up quickly and down slowly (stabilisation window of several minutes). - Ensure the cluster can add nodes (Cluster Autoscaler, Karpenter, Autopilot) and test with a load generator.
Step-by-step solution
1. Requests that reflect reality
The HPA computes CPU utilisation as usage divided by requests. If requests are far too low, utilisation looks high and the HPA overscales; too high, it never scales.
resources:
requests: { cpu: "500m", memory: "512Mi" }
limits: { memory: "1Gi" }
Measure under a realistic load test: run Gunicorn with your production worker count and see sustained CPU per pod. Many teams omit CPU limits (to avoid throttling) but keep memory limits.
2. CPU-based HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: web }
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: web }
minReplicas: 2
maxReplicas: 12
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 65 } }
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies: [{ type: Percent, value: 100, periodSeconds: 60 }]
scaleDown:
stabilizationWindowSeconds: 300
policies: [{ type: Pods, value: 1, periodSeconds: 60 }]
Doubling quickly on spikes and removing one pod per minute after five quiet minutes avoids flapping. minReplicas: 2 keeps redundancy across nodes.
3. Scale on requests when CPU lies
Django pods waiting on external APIs or slow queries can be saturated (all workers busy) at low CPU. Scale on a load metric instead: requests per second per pod or Gunicorn busy workers, exported to Prometheus and exposed to the HPA with Prometheus Adapter, or with KEDA's Prometheus scaler:
metrics:
- type: Pods
pods:
metric: { name: http_requests_per_second }
target: { type: AverageValue, averageValue: "40" }
Set the target from load tests: the request rate one pod handles with acceptable latency.
4. Protect the database
Each pod opens connections: Gunicorn workers × threads (× Celery if colocated). With 12 pods × 4 workers × 2 threads = 96 potential connections, plus workers and admin tasks. Either keep maxReplicas within the database's capacity or add PgBouncer in transaction mode so pods share a bounded pool.
5. Node capacity and startup
New pods need nodes and time: pulling the image, starting Gunicorn, passing readiness probes. Keep images small, use a readiness probe so traffic only reaches ready pods, and run a cluster autoscaler. A PodDisruptionBudget keeps enough replicas during node maintenance.
Verification check: run a load test that ramps traffic; kubectl get hpa web -w shows replicas rising as utilisation crosses the target and falling after the test, p95 latency stays acceptable during scale-up, and PostgreSQL connections stay below the limit.
Explanation
The HPA controller periodically reads metrics, computes desiredReplicas = ceil(currentReplicas × currentMetric / targetMetric), and applies behaviour policies to limit how fast replicas change. CPU utilisation is relative to requests, which is why accurate requests matter. For Django, CPU is a decent proxy when views are CPU-bound; when they wait on I/O, worker saturation or request rate tracks load better. Because each new pod multiplies connections to shared backends, autoscaling must be planned together with the database and connection pooling.
Edge cases / notes
- Celery workers scale better on queue length with KEDA than on CPU.
- Memory-based scaling rarely helps Django; memory grows with traffic slowly and does not shrink.
- Scale to zero for web is not possible with the HPA alone; use KEDA or Knative for that.
- Vertical Pod Autoscaler can recommend requests; do not run it in auto mode on the same metric as the HPA.
Next steps
- Scale workers by queue with Autoscale Celery Workers on Kubernetes With KEDA.
- Find per-pod capacity with Load Test Django Before Deploy.
FAQ
How do I autoscale Django on Kubernetes?
Create a HorizontalPodAutoscaler for the Deployment, with realistic CPU requests or a request-rate metric.
What CPU target should the HPA use?
Around 60-70 percent average utilisation, leaving headroom while new pods start.
Why does my HPA not scale even though the site is slow?
Workers may be blocked on I/O at low CPU. Scale on request rate or busy workers instead.
Can autoscaling overload PostgreSQL?
Yes. Cap maxReplicas or use PgBouncer so connections stay within limits.