Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Horizontal Pod Autoscaling in Kubernetes

By Kokil Thapa | Last reviewed: August 2026

Horizontal Pod Autoscaling in Kubernetes automatically adjusts the number of pod replicas based on observed CPU, memory, or custom metrics to match actual demand. For PHP and Laravel applications running in production, getting this right prevents both downtime during traffic spikes and wasted resources during quiet periods. If you are hiring a Laravel developer or managing infrastructure yourself, understanding HPA mechanics is essential because misconfigured autoscaling causes more outages than it prevents.

How does Horizontal Pod Autoscaling in Kubernetes actually work?

The HPA controller runs as a control loop inside the kube-controller-manager. Every 15 seconds by default, it queries the Metrics API for current metric values, compares them against your defined targets, and calculates the desired replica count. The formula is straightforward: desiredReplicas = ceil[currentReplicas × (currentMetricValue / desiredMetricValue)]. If you have 3 pods averaging 80% CPU against a 50% target, the controller computes ceil[3 × (80/50)] = 5 replicas and instructs the Deployment to scale up.

Application PodsCPU / Memory / CustomMetrics SourceMetrics APImetrics.k8s.ioAggregation LayerHPA Controller15s Control LoopReplica CalculationDeploymentScale SubresourceReplicaSetHorizontal Pod Autoscaling in Kubernetes Control LoopDesired Replicas = ceil[Current × (Current Metric / Target Metric)]Stabilization windows prevent flapping during transient spikes
Horizontal Pod Autoscaling in Kubernetes control loop: metrics flow from pods through the Metrics API to the HPA controller, which adjusts Deployment replicas every 15 seconds

In practice, the raw calculation gets smoothed by stabilization windows and tolerance settings. Without these, a momentary spike during deployment or cache warming triggers aggressive scale-up followed immediately by scale-down, causing request failures and log noise. I've seen this repeatedly on Laravel applications where queue workers briefly saturate CPU during batch processing, only to idle moments later. The HPA must distinguish sustained load from transient bursts.

Metric sources available in 2026

  • Resource metrics: CPU and memory utilization per container, provided by metrics-server. This is the baseline most tutorials cover and sufficient for many stateless PHP-FPM workloads.
  • Custom metrics: Application-specific values like active queue jobs, concurrent requests, or database connection pool usage, exposed via Prometheus Adapter or KEDA. Essential when CPU doesn't correlate with actual business load.
  • External metrics: Values from outside the cluster such as SQS queue depth, Redis list length, or third-party API rate limit consumption. Useful for event-driven scaling independent of pod resource usage.

How do you configure HPA for PHP-FPM and Laravel applications?

PHP-FPM workloads behave differently than long-running Go or Java services. Each FPM worker handles one request at a time, so CPU utilization often stays moderate even under heavy load because workers block on I/O. Relying solely on CPU targets frequently leaves you under-scaled during database-heavy operations or over-scaled during static asset serving. On production Laravel projects, I combine CPU with custom metrics that reflect actual application pressure.

<!-- hpa-laravel.yaml -->
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: laravel-app-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: laravel-app
  minReplicas: 2
  maxReplicas: 12
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Percent
          value: 50
          periodSeconds: 60
        - type: Pods
          value: 2
          periodSeconds: 60
      selectPolicy: Max
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 25
          periodSeconds: 120
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 65
    - type: Pods
      pods:
        metric:
          name: php_fpm_active_processes
        target:
          type: AverageValue
          averageValue: "8"

This configuration addresses three common failure modes. The 60-second scale-up stabilization prevents reaction to deployment-related CPU spikes. The 300-second scale-down window avoids premature teardown after traffic dips — PHP-FPM process startup isn't free, and losing warm workers hurts latency. The dual-metric approach ensures scaling responds to either compute saturation or FPM worker exhaustion, whichever hits first.

Exposing PHP-FPM metrics to Kubernetes

You need the Prometheus Adapter or KEDA to make php_fpm_active_processes available to the HPA. First, ensure your PHP-FPM status endpoint is enabled in the pool configuration:

; www.conf
pm.status_path = /fpm-status
ping.path = /fpm-ping

Then deploy a sidecar or standalone exporter like hipages/php-fpm_exporter that scrapes this endpoint and exposes Prometheus-format metrics. The adapter maps these into the Kubernetes Metrics API. Verify availability before deploying the HPA:

kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/production/pods/*/php_fpm_active_processes"

If this returns empty or errors, the HPA will show <unknown> for the custom metric and refuse to scale. I've debugged this exact issue on client projects where the exporter ran but the ServiceMonitor selector didn't match the pod labels. Always validate the metric pipeline end-to-end before trusting autoscaling in production.

PHP-FPM Pool/fpm-statusActive WorkersFPM ExporterPrometheus FormatSidecar/StandalonePrometheusTime-Series DBScrape Interval 15sK8s Adaptercustom.metrics.k8s.io→ HPA ControllerCustom Metric Pipeline for PHP-FPM AutoscalingValidate each stage: exporter → Prometheus → Adapter → HPAMissing ServiceMonitor selectors are the #1 cause of <unknown> metrics
Custom metric pipeline for Horizontal Pod Autoscaling in Kubernetes: PHP-FPM status flows through an exporter, Prometheus, and the adapter before reaching the HPA controller

What are the common HPA mistakes that cause production incidents?

After maintaining Kubernetes deployments for Laravel and Symfony applications, certain failure patterns recur. These aren't theoretical — they're the issues I troubleshoot when DevOps automation breaks down or clients report intermittent slowness.

Setting minReplicas too low for stateful or slow-starting apps

A minReplicas of 1 seems cost-efficient until that single pod restarts during a rolling update or OOM kill. For any application handling user sessions, payment callbacks, or legal document processing, minimum 2 replicas provides basic resilience. The HPA can still scale down to this floor during off-hours, but you never drop below survivable capacity. On Nepal-based legal-tech portals handling court marriage applications, I set minReplicas to 3 because even brief unavailability during peak filing hours generates support calls.

Ignoring scale-down stabilization

Default scale-down behavior in older HPA versions was aggressive. Even with autoscaling/v2 defaults improved in 2026, explicit stabilization windows prevent thrashing. A 5-minute scale-down window means the controller waits 300 seconds of sustained low metrics before removing pods. This costs slightly more during genuine lulls but eliminates the far greater cost of premature scale-down followed by immediate scale-up when traffic resumes. Calculate your typical traffic dip duration and set the window accordingly.

Mismatched resource requests and limits

HPA CPU utilization is calculated against resource requests, not limits. If your container requests 250m CPU but has a 2000m limit, hitting 100% utilization means using 250m — potentially far below actual saturation. Set requests close to observed p50 usage and limits at p99 plus headroom. Misaligned values make the HPA either trigger too late (requests too low) or waste resources (requests too high). Audit this with kubectl top pods correlated against actual response latency.

MistakeSymptomFix
CPU-only scaling for I/O-bound PHPHigh latency despite low CPU utilizationAdd custom metrics (active FPM workers, queue depth)
No scale-down stabilizationRapid scale-up/down cycles, increased error ratesSet scaleDown.stabilizationWindowSeconds ≥ 300
minReplicas = 1 for critical servicesDowntime during single-pod restartsMinimum 2 replicas; 3+ for revenue/legal workflows
Resource requests ≠ actual usageHPA triggers too early or too lateAlign requests to p50 observed CPU/memory
Missing metric validationHPA shows <unknown>, refuses to scaleTest metric endpoint before deploying HPA

When should you use custom metrics instead of CPU for autoscaling?

CPU works when compute correlates linearly with request volume. For many PHP applications, it doesn't. Database-heavy endpoints, file processing, and external API calls create load patterns where CPU stays moderate while the application is effectively saturated. In these cases, custom metrics provide accurate scaling signals.

Start: Choose MetricIs workload CPU-bound?YesNoUse CPU UtilizationTarget 60-75%I/O or Queue Bound?YesNoCustom MetricsQueue Depth / Active WorkersMemory ScalingCache / Session Heavy
Decision framework for selecting metrics in Horizontal Pod Autoscaling in Kubernetes: CPU for compute-bound, custom metrics for I/O and queue workloads, memory for cache-heavy applications

For Laravel queue workers, scale on pending job count rather than CPU. A worker processing video transcoding may sit at 30% CPU while blocking on ffmpeg, but ten pending jobs indicate backlog requiring more workers. Expose queue depth via a Prometheus exporter reading from Redis or your queue backend, then reference it in the HPA as an external or pods metric. This aligns replica count with actual business backlog, not misleading resource utilization.

Similarly, for applications with connection pooling or rate-limited dependencies, scale on those constraints directly. If your MySQL pool maxes at 50 connections per pod and you observe connection wait times increasing, that's a better scaling signal than CPU which might remain flat. The principle: scale on the bottleneck, not a proxy. When building Laravel queue systems for high traffic, this distinction determines whether autoscaling actually resolves performance issues or just adds expensive idle pods.

How do you test and validate HPA behavior before production?

Never deploy an untested HPA directly to production traffic. The feedback loop is slow (15-second intervals plus stabilization windows), and misconfigurations manifest as user-facing errors. Follow a structured validation process.

  1. Verify metric availability: Run kubectl get hpa and confirm all metrics show numeric values, not <unknown>. If unknown, debug the metric pipeline before proceeding.
  2. Load test with realistic patterns: Use tools like k6 or Locust to generate traffic matching production profiles. Don't just hammer a single endpoint — simulate mixed read/write ratios, authenticated vs anonymous traffic, and background job submission.
  3. Observe scaling timing: Record when load increases, when the HPA detects it, and when new pods become ready. The gap between detection and readiness is your effective response latency. If it exceeds acceptable thresholds, optimize pod startup time or adjust stabilization windows.
  4. Test scale-down behavior: After load subsides, verify pods scale down according to your stabilization policy. Confirm no requests fail during termination (ensure preStop hooks and graceful shutdown periods exceed your longest request duration).
  5. Simulate failure scenarios: Kill pods manually during load to test recovery. Verify the HPA doesn't overcorrect. Test what happens when the Metrics API becomes temporarily unavailable — the HPA should maintain current replicas, not scale to zero or max.

Document observed behavior alongside your HPA manifest. When on-call engineers respond to scaling alerts at 2 AM, they need context about expected behavior versus anomalies. This documentation pays for itself during incident response and reduces mean time to resolution significantly.

Implementing reliable Horizontal Pod Autoscaling in Kubernetes

Horizontal Pod Autoscaling in Kubernetes delivers genuine operational value when configured with application-aware metrics, appropriate stabilization, and validated behavior. Start with conservative targets and explicit bounds, measure actual performance under load, then tune iteratively. Avoid copying example manifests without adapting them to your workload's specific characteristics — PHP-FPM isn't Node.js, and legal-tech portals have different availability requirements than internal dashboards.

If you're implementing Kubernetes autoscaling for Laravel, Symfony, or other PHP applications and want to avoid the production pitfalls described here, reach out to discuss your infrastructure. Getting HPA right upfront prevents costly debugging later and ensures your scaling investment actually improves reliability rather than introducing new failure modes.

Frequently Asked Questions

Horizontal Pod Autoscaling automatically adjusts the number of pod replicas in a deployment based on observed CPU utilization, memory usage, or custom metrics to match current demand.

HPA changes replica count horizontally while VPA adjusts resource requests and limits vertically within existing pods without changing the total number of running instances.

Use KEDA when scaling must trigger on external event sources like queue depth or HTTP request rate rather than standard pod-level CPU or memory utilization metrics.

You need Metrics Server installed and running, resource requests defined on target containers, and a compatible Kubernetes version supporting the autoscaling/v2 API for stable metric definitions.

Define an HPA manifest specifying scaleTargetRef, minReplicas, maxReplicas, and metrics array with type Resource, Pods, or External. Apply it via kubectl apply. The controller then queries Metrics Server every fifteen seconds by default to evaluate scaling thresholds against your defined targets. Always set explicit resource requests on containers or HPA cannot calculate utilization percentages correctly.

This usually happens when containers lack CPU or memory requests, causing Metrics Server to return no utilization data. Verify requests exist with kubectl describe pod. Also check that Metrics Server pods are healthy and can reach kubelet metrics endpoints. If using custom metrics, ensure the adapter service is running and the API registration shows available resources via kubectl get apiservices.

The formula is desiredReplicas equals ceil[currentReplicas multiplied by (currentMetricValue divided by desiredMetricValue)]. For multiple metrics, HPA calculates each independently and selects the highest result. Rounding always goes up. Understanding this math helps debug unexpected scaling decisions when current values fluctuate near threshold boundaries during production traffic spikes.

Yes, using the External or Object metric types in autoscaling/v2 requires a custom metrics adapter like Prometheus Adapter. Your application exposes metrics via an endpoint, Prometheus scrapes them, and the adapter translates PromQL queries into Kubernetes metric API responses. Configure the HPA metric spec with matcher labels to filter specific time series. Test thoroughly since misconfigured queries cause silent scaling failures.

Default sync period is fifteen seconds and Metrics Server aggregates over one-minute windows, creating inherent delay. Reduce --horizontal-pod-autoscaler-sync-period in kube-controller-manager and tune Metrics Server resolution if faster response is critical. Consider predictive scaling or over-provisioning buffer replicas for latency-sensitive workloads where even thirty-second delay causes user-visible degradation during flash traffic events.

Set stabilizationWindowSeconds under behavior.scaleDown in autoscaling/v2 to enforce a cooldown period before reducing replicas. Default is three hundred seconds. Increase this value for stateful or slow-starting applications where premature termination causes errors. Also configure policies like Percent or Pods with periodSeconds to control maximum reduction rate per interval, preventing cascading failures during transient load dips.

Yes, they complement each other. HPA increases pod replicas which may trigger pending pods if node capacity is exhausted. Cluster Autoscaler detects unschedulable pods and provisions new nodes. Ensure node pools have sufficient headroom and appropriate instance types. Monitor both controllers together since HPA requesting twenty replicas on a three-node cluster will stall until Cluster Autoscaler adds capacity, adding minutes of provisioning delay.

Restrict RBAC permissions for the autoscaling API to prevent unauthorized scaling manipulation. Validate that custom metric adapters authenticate properly and expose only intended metrics. Avoid exposing sensitive application data through metric labels. Audit HPA manifests in CI pipelines since misconfigured maxReplicas could be exploited for resource exhaustion attacks. In multi-tenant clusters, use namespace-scoped HPAs and network policies isolating metric adapter services from untrusted workloads.

Over-provisioned maxReplicas or missing scale-down stabilization wastes cloud spend continuously. On AWS EKS, ten unnecessary m5.large pods cost roughly NPR 45,000 monthly (~USD 335). Under-provisioned minReplicas causes revenue loss during peak hours. Budget five to ten percent extra capacity as buffer but implement proper stabilization windows. Review actual utilization weekly with kubectl top and adjust thresholds incrementally rather than setting conservative static values permanently.

Run kubectl get hpa to check TARGETS column for slash notation showing current versus desired values. If current shows unknown, Metrics Server cannot retrieve metrics. Check kubectl logs deployment/metrics-server for connection errors. Verify container resource requests match actual consumption patterns. Confirm the HPA references correct deployment name and namespace. Test manually with kubectl scale to rule out scheduler or quota constraints preventing new pods from starting successfully.

Setting minReplicas equal to maxReplicas disables scaling entirely. Using averageValue instead of averageUtilization without understanding absolute versus percentage semantics causes unexpected behavior. Scaling stateful databases with HPA risks data corruption. Ignoring startup probes leads to premature scaling before containers are ready. Deploying HPA without load testing produces unvalidated thresholds. Always pair HPA with PodDisruptionBudgets, readiness probes, and gradual rollout strategies to avoid production incidents during scaling events.

Share this article

Quick Contact Options
Choose how you want to connect me: