Autoscale a PHP Service on Kubernetes

Khimananda Oli 8 min read Programming and Languages
Autoscale a PHP Service on Kubernetes

By Khimananda Oli | Last reviewed: August 2026

PHP applications behave differently than stateless microservices because they rely on PHP-FPM worker pools rather than thread-per-request models. When you attempt to autoscale a PHP service on Kubernetes, standard CPU-based Horizontal Pod Autoscaling (HPA) often fails to capture actual request saturation, leading to dropped connections during traffic spikes. This guide covers the specific configuration required to align Kubernetes scaling logic with PHP-FPM internals, ensuring your Laravel or Symfony application scales based on real capacity rather than misleading system metrics.

Ingress TrafficHTTP RequestsPHP PodNginx / CaddyPHP-FPM Poolpm.max_children: 50PrometheusScrapes /metricsK8s HPAScale DecisionReplica Count Adjustment
Figure 1: Architecture flow to autoscale a PHP service on Kubernetes using custom PHP-FPM metrics scraped by Prometheus.

Why does default CPU scaling fail when you autoscale a PHP service on Kubernetes?

Most Kubernetes tutorials assume a linear relationship between CPU usage and request throughput. For Go or Node.js services, this assumption holds reasonably well. PHP-FPM breaks this model entirely. A PHP container can be completely saturated with requests while reporting only 40% CPU utilization because workers are blocked waiting on database queries, external APIs, or file I/O.

When you configure resource limits and requests without understanding PHP's process model, you create a dangerous gap. The HPA sees "healthy" CPU metrics and refuses to scale out, while inside the pod, every PHP-FPM child process is occupied. New requests queue at the Nginx level or get rejected with 502 Bad Gateway errors. I have seen this exact scenario cause outages during product launches for Nepali e-commerce platforms where traffic spiked suddenly but database latency kept CPU usage deceptively low.

The solution requires exposing PHP-FPM's internal state to Kubernetes. You need metrics that reflect actual worker saturation: active processes, idle processes, and listen queue length. These metrics provide a true picture of remaining capacity, allowing the HPA to scale before users experience errors. This approach aligns with the principles discussed in defining meaningful SLIs and SLOs, where you measure what users actually experience rather than proxy system statistics.

How do you expose PHP-FPM metrics for autoscaling?

Kubernetes cannot natively read PHP-FPM status pages. You need an exporter sidecar or embedded library that translates FPM stats into Prometheus format. The most reliable approach in 2026 uses the php-fpm-exporter or integrates metric collection directly into your application via libraries like prometheus-php-fpm.

Configure PHP-FPM Status Endpoint

First, enable the status endpoint in your PHP-FPM pool configuration. This exposes the raw data exporters need:

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

Restrict access to these endpoints in your Nginx configuration to prevent public exposure:

location /fpm-status {
    allow 127.0.0.1;
    deny all;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}

location /metrics {
    allow 127.0.0.1;
    deny all;
    proxy_pass http://127.0.0.1:9253/metrics;
}

Deploy the Metrics Exporter

Add the exporter as a sidecar container in your deployment. This keeps metrics collection isolated from your application logic:

spec:
  containers:
  - name: php-app
    image: myregistry/php-app:latest
    ports:
    - containerPort: 8080
  - name: fpm-exporter
    image: hipages/php-fpm_exporter:2.3
    args:
      - "--phpfpm.scrape-uri=tcp://127.0.0.1:9000/fpm-status"
      - "--web.listen-address=:9253"
    ports:
    - containerPort: 9253
      name: metrics

Verify metrics are flowing correctly before proceeding. Run kubectl port-forward and check /metrics. You should see php_fpm_active_processes, php_fpm_idle_processes, and php_fpm_listen_queue. If these values are missing or zero, your status path configuration is incorrect or the exporter cannot reach the FPM socket. Proper observability here mirrors practices from Prometheus metrics monitoring fundamentals.

Start: Configure HPA MetricIs workload CPU-bound? (Image processing)YesNo (Typical Web)Use CPU MetricCheck DB LatencyHigh DB wait? (>30% time)YesNoUse Listen Queue LengthUse Active Processes %Always set behavior.stabilizationWindowSeconds
Figure 2: Decision flowchart for selecting the correct scaling metric when you autoscale a PHP service on Kubernetes.

How do you configure HPA with custom PHP-FPM metrics?

With metrics exposed, configure the Horizontal Pod Autoscaler to use them. The key is targeting the right threshold. For most PHP web applications, scaling when active processes reach 70-80% of pm.max_children provides headroom for burst traffic without over-provisioning.

Install Prometheus Adapter

Your cluster needs the Prometheus Adapter to translate Prometheus queries into Kubernetes custom metrics API responses. If you use kube-prometheus-stack, this is included. Otherwise, install it separately via Helm:

helm install prometheus-adapter prometheus-community/prometheus-adapter \
  --namespace monitoring \
  --set prometheus.url=http://prometheus-server.monitoring.svc \
  --set prometheus.port=9090

Create the HPA Manifest

This HPA configuration targets 75% worker utilization with proper stabilization to prevent flapping:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: php-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-app
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Pods
    pods:
      metric:
        name: php_fpm_active_processes
      target:
        type: AverageValue
        averageValue: "38"  # 75% of pm.max_children=50
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
      - type: Pods
        value: 4
        periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Pods
        value: 2
        periodSeconds: 120

The asymmetric stabilization windows are critical. Scale-up happens quickly (60 seconds) to handle traffic surges. Scale-down waits five minutes to avoid terminating pods during temporary lulls. This asymmetry prevents the "sawtooth" pattern that causes unnecessary pod churn and cold-start penalties. For teams managing multiple environments, Kustomize can help manage environment-specific HPA thresholds without duplicating manifests.

What PHP-FPM settings must align with Kubernetes resources?

Misaligned PHP-FPM and Kubernetes configurations are the most common failure mode. Your pm.max_children setting must account for actual per-process memory usage, not theoretical minimums. A single PHP worker handling a complex Laravel request can consume 80-150MB depending on loaded libraries and dataset size.

Container Memory LimitRecommended pm.max_childrenAvg Memory/WorkerOverhead ReserveRisk Level
512Mi4-680MB100MBHigh (tight margin)
1Gi10-1280MB150MBMedium
2Gi22-2580MB200MBLow (recommended)
4Gi45-5080MB300MBVery Low

Calculate your own numbers using this formula: (Memory Limit - Overhead) / Average Worker Memory = max_children. Measure actual worker memory in staging under realistic load using ps --no-headers -o rss -C php-fpm | awk '{sum+=$1} END {print sum/NR/1024 " MB"}'. Never guess. OOMKilled pods during peak traffic indicate you got this math wrong.

Set pm = dynamic with pm.start_servers at 30-50% of max_children, pm.min_spare_servers at 20%, and pm.max_spare_servers at 60%. Static process management wastes memory during low traffic and cannot adapt within a pod's lifetime. Dynamic management complements cluster-level autoscaling by optimizing resource usage within each pod.

How do you validate autoscaling works before production?

Never trust autoscaling configuration without load testing. Synthetic validation catches misconfigurations that only manifest under pressure. Use tools like k6 or Locust to simulate realistic traffic patterns including gradual ramps, sudden spikes, and sustained plateaus.

  1. Baseline test: Send steady traffic at 50% target capacity for 10 minutes. Verify pod count remains stable and metrics report expected values.
  2. Spike test: Jump from 30% to 90% capacity in 30 seconds. Confirm HPA triggers scale-up within the stabilization window and no requests return 5xx errors.
  3. Sustain test: Hold 80% capacity for 15 minutes. Ensure scaling stabilizes and doesn't continue climbing due to metric lag.
  4. Cool-down test: Drop traffic to 20%. Verify scale-down respects the longer stabilization window and doesn't prematurely terminate pods.
  5. Failure injection: Kill random pods during sustained load. Confirm remaining pods absorb traffic and HPA replaces lost capacity.

Monitor three signals during testing: HPA events (kubectl get events | grep HPA), actual vs desired replicas (kubectl get hpa -w), and error rates in your ingress controller logs. If errors spike before scaling completes, either reduce the target utilization threshold or decrease the scale-up stabilization window. Document these findings; they become your operational runbook.

Time During Traffic SpikePod Count01020Custom Metric (FPM)CPU Only (Delayed)Error Window
Figure 3: Custom metrics trigger faster scaling than CPU alone, eliminating the error window during traffic spikes when you autoscale a PHP service on Kubernetes.

Reliable Scaling Requires Alignment

Successfully implementing autoscaling for PHP on Kubernetes demands alignment across three layers: PHP-FPM process configuration, Kubernetes resource definitions, and HPA metric selection. Missing any one layer creates failure modes that only appear under production load. Start with accurate worker memory measurements, expose genuine saturation metrics, and validate with realistic load tests before trusting the system. If your team needs help auditing existing PHP infrastructure or designing compliant autoscaling architectures, reach out to discuss your specific requirements.

Frequently Asked Questions

Yes, use the autoscaling/v2 API with custom metrics from Prometheus adapter targeting php_fpm_active_processes.

Active PHP-FPM worker processes are most accurate because they directly reflect request handling capacity, unlike CPU which misrepresents blocking I/O workloads common in Laravel applications.

KEDA excels when scaling based on external queue depth or HTTP request rate rather than pod metrics, making it ideal for Laravel Horizon workers or event-driven PHP microservices.

New PHP-FPM pods require warmup time to compile OPcache and establish database connections. Configure readiness probes and preloading to prevent routing traffic to unready containers during scale-out events.

Set CPU requests matching your pm.max_children multiplied by average per-worker usage, typically 50m to 100m per worker. Memory requests must account for peak RSS per process plus shared OPcache overhead.

Increase stabilizationWindowSeconds to 300 for scale-down and 60 for scale-up in your HPA spec. Also tune PHP-FPM pm.process_idle_timeout to align with Kubernetes cooldown periods to avoid premature worker termination.

Horizontal scaling is preferred for stateless PHP-FPM since vertical scaling requires pod restarts that drop active connections. Use VPA only in recommendation mode to right-size resource requests for HPA efficiency.

Cold pods without preloaded OPcache consume extra CPU during compilation, triggering false scale-up signals. Enable opcache.preload and use init containers to warm caches before the pod passes readiness checks.

Kubernetes sends SIGTERM but PHP-FPM may kill workers immediately. Configure a preStop hook with sleep 5 and set terminationGracePeriodSeconds to allow graceful drain of active FastCGI connections.

Yes, deploy KEDA with a Redis scaler targeting your Laravel queue size. This decouples worker scaling from HTTP traffic patterns and ensures background jobs process promptly during traffic spikes.

Use kubectl run to generate synthetic load or tools like Locust targeting your service endpoint. Monitor HPA status with kubectl get hpa -w and verify metric server returns valid custom metrics.

Costs rise linearly with replica count, but proper resource requests prevent over-provisioning. Use cluster autoscaler with node pools sized for PHP workloads and spot instances for non-critical worker nodes.

Use connection pooling via PgBouncer or ProxySQL since each new pod creates fresh database connections. Implement service mesh mTLS and limit database credentials using Kubernetes secrets with short-lived tokens.

Use static mode with pm.max_children matching container resource limits. Dynamic modes cause unpredictable memory spikes that trigger OOMKills during scale events, undermining HPA stability and wasting provisioned resources.

Track HPA desired versus current replicas, p99 latency, and PHP-FPM busy worker percentage in Grafana. Alert when desired replicas hit maxReplicas or when scale-up lag exceeds acceptable SLO thresholds.