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.

HPA for Django podsMetricCPU or req/sHPA controllertarget per podReplicas changemin to maxLimitsDB connections, nodesThe HPA keeps a per-pod target; requests, max replicas, and the database decide how it works.
  1. Set realistic CPU and memory requests on Django containers, measured under load.
  2. Start with CPU-based scaling: averageUtilization: 60-70 with minReplicas of at least 2.
  3. For I/O-bound apps, scale on requests per second per pod via Prometheus Adapter or KEDA instead of CPU.
  4. Cap maxReplicas so total database connections (replicas × workers × threads) stay under the limit, or add PgBouncer.
  5. Use behavior to scale up quickly and down slowly (stabilisation window of several minutes).
  6. 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.

a campaign spike, minute by minuteBaseline2 pods, 40% CPUSpike startsCPU 90%Scale up x24 then 8 podsTraffic fallsCPU 30%Slow scale downafter 5 min windowFast up, slow down: capacity arrives quickly and leaves cautiouslydjango-deployment.com
New pods need time to start; keep minReplicas high enough for sudden spikes.

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.

HPA that behavesCPU requests measured under loadutilisation math depends on itMetric that tracks saturationCPU or requests per podFast up, slow down behaviourno flappingmaxReplicas within DB connectionsor PgBouncerNodes can scale, pods start fastsmall images, readinessAutoscaling moves the bottleneck; make sure the database is not nextdjango-deployment.com
Load-test the scale-up path before relying on it for a launch.

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

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.

Learning path · step 12 of 16

Django on Kubernetes

Keep going