
Table of Contents
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.
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
startupProbeconfigurations. 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.
| Metric | Type | Scale Trigger Threshold | Why It Matters |
|---|---|---|---|
| CPU Utilization | Resource | > 80% sustained | Baseline safety net; catches runaway loops |
| GenServer Mailbox Length | Custom | > 1000 msgs avg | Indicates processing bottleneck before latency spikes |
| ETS Table Memory | Custom | > 80% of allocated | Prevents OOM from unbounded cache growth |
| Phoenix Request Duration p95 | Custom | > SLO target | Directly maps to user experience and error budgets |
| Connection Pool Saturation | Custom | > 90% checked out | DB/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
)
]
]}
] 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.
- Install Metrics Adapter: Ensure prometheus-adapter or keda is deployed and configured to scrape your Elixir metrics endpoint.
- Define Multiple Metrics: Use
AverageValuefor custom metrics across pods, notUtilization. - 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.
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.