Autoscale a Elixir Service on Kubernetes

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

By Khimananda Oli | Last reviewed: August 2026

To successfully autoscale a Elixir service on Kubernetes, you must align the Horizontal Pod Autoscaler (HPA) with the unique concurrency model of the BEAM virtual machine. Standard CPU-based scaling often fails for Elixir because the runtime manages its own schedulers and memory internally, masking true saturation from generic container metrics. You need to configure precise resource requests, expose application-specific telemetry via OpenTelemetry or Prometheus, and tune HPA stabilization windows to prevent oscillation during traffic bursts. This guide covers the exact configuration required to make Elixir scaling predictable and safe.

Elixir PodBEAM VM + AppOTel CollectorMetrics ExportPrometheusMetric StoreK8s HPAScaling LogicCustom BEAM Metrics Drive Accurate Scaling Decisions
High-level flow for autoscaling Elixir services using BEAM-aware metrics instead of raw CPU alone.

Why is it difficult to autoscale a Elixir service on Kubernetes?

The primary challenge when you autoscale a Elixir service on Kubernetes stems from the mismatch between container-level visibility and BEAM-level reality. The Erlang VM is designed to utilize all available schedulers (logical cores) up to 100% even under normal load, as it proactively schedules lightweight processes. A naive HPA configured to scale at 70% CPU will trigger constantly, creating unnecessary pod churn without actually improving throughput. Conversely, if your workload is memory-bound due to large ETS tables or message backlogs, CPU might remain low while the pod approaches OOMKill thresholds.

Furthermore, Elixir applications have non-trivial startup times. Compiling modules, warming up caches, connecting to databases, and joining distributed clusters can take 15–45 seconds. If your HPA reacts too quickly to transient spikes, new pods may not be ready to serve traffic before the next scaling decision occurs. This "thrashing" degrades performance more than staying slightly over-provisioned. Understanding these resource limit interactions is critical before writing any autoscaling YAML.

BEAM Scheduler Alignment

The BEAM creates one scheduler per logical CPU core by default. In Kubernetes, if you set a CPU limit of 500m (half a core), the VM still detects the underlying node's core count and spawns many schedulers competing for fractional time slices. This causes excessive context switching. Always set CPU requests equal to limits for Elixir workloads, and match them to whole numbers where possible (e.g., 1, 2, 4 cores). Use the environment variable ERL_AFLAGS="+S ${ERLANG_SCHEDULERS}" to explicitly bind scheduler count to your container allocation.

How do you configure resource requests for Elixir pods?

Correct resource configuration is the foundation. Without it, no amount of HPA tuning will save you. When preparing to autoscale a Elixir service on Kubernetes, treat requests as guaranteed capacity and limits as hard ceilings that should rarely be hit.

  • CPU Requests = Limits: Prevents throttling of BEAM schedulers. Set both to the same value (e.g., 1000m).
  • Memory Headroom: The BEAM allocator has overhead. Set memory limits 20–30% above your observed p99 usage. If your app uses 400Mi steady-state, set requests to 512Mi and limits to 640Mi.
  • Startup Probes: Use generous startupProbe configurations. Elixir apps doing DB migrations or cache hydration need time. A failed startup probe kills the pod before it ever serves traffic.
<!-- k8s/elixir-deployment.yaml -->
resources:
  requests:
    cpu: "1000m"
    memory: "512Mi"
  limits:
    cpu: "1000m"      # Match requests to avoid scheduler contention
    memory: "680Mi"   # ~30% headroom for GC spikes
env:
  - name: ERLANG_SCHEDULERS
    value: "1"        # Matches 1000m CPU allocation
  - name: ERL_AFLAGS
    value: "+S 1 +P 1048576"

This configuration ensures each pod gets dedicated compute capacity. For teams managing multiple environments, integrating this into a parameterized Helm chart prevents drift between staging and production resource profiles.

Which metrics should drive Elixir autoscaling decisions?

Relying solely on CPU and memory is insufficient. To truly autoscale a Elixir service on Kubernetes effectively, you must expose business and runtime metrics that reflect actual user-perceived latency and system pressure. The BEAM exposes rich internal state that correlates far better with scaling needs than host-level stats.

MetricTypeScale Trigger ThresholdWhy It Matters
CPU UtilizationResource> 80% sustainedBaseline safety net; catches runaway loops
GenServer Mailbox LengthCustom> 1000 msgs avgIndicates processing bottleneck before latency spikes
ETS Table MemoryCustom> 80% of allocatedPrevents OOM from unbounded cache growth
Phoenix Request Duration p95Custom> SLO targetDirectly maps to user experience and error budgets
Connection Pool SaturationCustom> 90% checked outDB/Redis backpressure requires horizontal scale

Exposing Custom Metrics

Use the telemetry_metrics_prometheus library to export BEAM internals. Configure Telemetry events in your Application supervisor to capture mailbox sizes periodically. These metrics flow through your existing monitoring stack and become available to the Kubernetes Metrics Adapter for HPA consumption.

# lib/my_app/application.ex
children = [
  {TelemetryMetricsPrometheus.Core, [
    metrics: [
      Telemetry.Metrics.last_value("vm.memory.total", unit: :byte),
      Telemetry.Metrics.sum("phoenix.endpoint.stop.duration", 
        event_name: [:phoenix, :endpoint, :stop],
        measurement: :duration,
        tags: [:method, :route]
      ),
      # Custom GenServer mailbox metric
      Telemetry.Metrics.last_value("app.worker.mailbox_length",
        event_name: [:app, :worker, :mailbox],
        measurement: :length
      )
    ]
  ]}
]
BEAM TelemetryProm Ex / OTelPrometheusMetrics AdapterHPA ControllerScrape Interval: 15s | Evaluation Period: 60s | Stabilization Window: 300sMetric Pipeline Ensures HPA Sees Fresh, Aggregated BEAM Data
Data flow from BEAM telemetry events to Kubernetes HPA scaling decisions with recommended timing parameters.

How do you write an HPA manifest for Elixir workloads?

The HPA manifest for Elixir differs from typical microservices. You must combine resource metrics with custom metrics and apply behavioral constraints to prevent instability. Below is a production-tested configuration to autoscale a Elixir service on Kubernetes safely.

  1. Install Metrics Adapter: Ensure prometheus-adapter or keda is deployed and configured to scrape your Elixir metrics endpoint.
  2. Define Multiple Metrics: Use AverageValue for custom metrics across pods, not Utilization.
  3. Set Behavior Policies: Configure scale-up and scale-down stabilization windows separately. Elixir scales up fast but down slow.
# k8s/elixir-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-elixir-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-elixir-app
  minReplicas: 2
  maxReplicas: 12
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 80
    - type: Pods
      pods:
        metric:
          name: app_worker_mailbox_length
        target:
          type: AverageValue
          averageValue: "1000"
    - type: Pods
      pods:
        metric:
          name: phoenix_request_duration_p95
        target:
          type: AverageValue
          averageValue: "500m"  # 500ms SLO target
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Percent
          value: 100
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300  # Wait 5min before removing pods
      policies:
        - type: Percent
          value: 25
          periodSeconds: 120           # Remove max 25% every 2min

The asymmetric behavior policy is crucial. Scale-up responds within 60 seconds to handle traffic surges, while scale-down waits 5 minutes to ensure the spike wasn't transient. This respects the cost of Elixir pod startup and prevents flapping. Teams practicing progressive delivery should coordinate HPA minReplicas with rollout strategies to maintain baseline capacity during deployments.

What are common pitfalls when scaling Elixir on Kubernetes?

Even with correct configuration, subtle issues emerge in production. Awareness of these failure modes separates theoretical knowledge from operational excellence when you autoscale a Elixir service on Kubernetes.

  • Distributed Erlang Clustering: If using libcluster, ensure new pods join the cluster before receiving traffic. Use readiness gates tied to cluster membership, not just HTTP health checks. Premature routing causes split-brain scenarios.
  • Shared State Assumptions: ETS tables are local to each pod. Scaling horizontally does not increase shared cache capacity. If your app relies on ETS for global state, implement a distributed cache layer or accept data duplication.
  • Connection Storms: Each new pod opens database and Redis connections. At 12 replicas with pool_size=20, you need 240 DB connections. Verify your RDS/Aurora max_connections and pgBouncer settings before raising maxReplicas.
  • Log Explosion During Scaling: New pods generate startup logs. Ensure your logging pipeline can handle burst volume without dropping critical application errors during scale events.
Healthy Scaling PatternSmooth ramp-up, stable plateau, gradual cooldownProblematic ThrashingRapid oscillation from aggressive thresholds or missing stabilizationKey Differentiators✓ Custom BEAM metrics over CPU-only✓ Asymmetric stabilization windows (fast up, slow down)✓ Resource requests aligned with scheduler count✗ Generic CPU thresholds ignoring BEAM semantics✗ Symmetric or absent behavior policies✗ Fractional CPU causing scheduler thrashing
Visual comparison of stable Elixir autoscaling versus destructive thrashing caused by misconfigured HPA parameters.

Start Autoscaling Your Elixir Service Safely Today

When you autoscale a Elixir service on Kubernetes correctly, you gain responsive capacity management that respects the BEAM's strengths rather than fighting them. Begin by auditing your current resource allocations against scheduler counts, then instrument mailbox and request latency metrics before enabling HPA. Test scaling behavior under synthetic load in staging with the same resource profile as production. Monitor the first week closely using Grafana dashboards overlaying HPA decisions against actual traffic patterns. If your team needs help designing observability-first Elixir infrastructure or validating your autoscaling configuration against compliance requirements, reach out for a consultation. Production-grade scaling is built on measurement, not guesswork.

Frequently Asked Questions

Deploy metrics-server, then create a HorizontalPodAutoscaler resource targeting your Elixir Deployment with cpu or memory thresholds.

Use custom Erlang VM mailbox size or request queue depth instead of CPU, as BEAM schedulers mask true load under standard metrics.

Yes, BEAM saturates all cores at low actual load, causing premature scaling; use application-level metrics via prometheus-elixir instead.

Add prometheus-elixir and prometheus_phoenix libraries, configure a /metrics endpoint, and set up ServiceMonitor for kube-prometheus-stack scraping.

Set minReplicas to at least two for high availability, ensuring rolling updates and node failures never cause complete service downtime.

Pre-warm connections in Application.start callback and use readiness probes checking GenServer state before accepting traffic from ingress.

Yes, configure KEDA ScaledObject with rabbitmq trigger pointing to your queue, setting threshold values matching your processing capacity per pod.

Check stabilizationWindowSeconds in HPA spec; default five-minute delay prevents flapping but may keep over-provisioned pods too long.

Configure Poolboy or DBConnection pool size proportional to max replicas, and implement connection backpressure using GenServer call timeouts.

Set limits to 2x expected heap usage accounting for binary heap fragmentation; monitor with :recon.memory() to avoid OOMKilled restarts.

Use k6 or artillery to generate synthetic load patterns while watching kubectl get hpa -w and verifying replica count changes match expectations.

No direct interaction, but ensure node pools have sufficient headroom since Elixir pods request fixed resources regardless of BEAM scheduler utilization.

Set terminationGracePeriodSeconds exceeding your longest GenServer call timeout and trap_exit signals to drain in-flight requests properly.

Verify metrics-server is running, RBAC allows namespace access, and prometheus adapter correctly maps custom metrics to HPA API format.

Avoid combining VPA with HPA on same resource; use VPA in recommendation mode only to right-size requests without conflicting scale decisions.