
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
To successfully autoscale a Kotlin service on Kubernetes, you must align the Horizontal Pod Autoscaler (HPA) with the specific runtime characteristics of the JVM. Unlike stateless Go or Node.js services, Kotlin applications running on the JVM have distinct memory management and startup behaviors that standard CPU-based scaling often mishandles. This guide provides the exact configuration patterns, metric definitions, and safety guardrails needed to scale Kotlin microservices reliably in production environments without triggering crash loops or excessive cloud spend.
How do you configure HPA to autoscale a Kotlin service on Kubernetes?
The foundation of scaling any JVM workload is the Horizontal Pod Autoscaler. However, a common mistake when configuring HPA for Kotlin is relying solely on default metrics without accounting for JIT warm-up or garbage collection pauses. Before applying an HPA, ensure your deployment has explicit resource requests and limits defined. Without these, the HPA cannot calculate utilization percentages, and the scheduler cannot make informed placement decisions. Refer to Kubernetes resource limits and requests for a deep dive on setting these correctly for JVM containers.
Setting baseline resource constraints
Kotlin services typically run on OpenJDK or Eclipse Temurin. You must pass container-aware JVM flags so the runtime respects cgroup boundaries. In 2026, most base images support this natively, but explicit configuration prevents edge-case OOM errors during rapid scale-up events.
apiVersion: apps/v1
kind: Deployment
metadata:
name: kotlin-order-service
spec:
replicas: 2
selector:
matchLabels:
app: kotlin-order-service
template:
metadata:
labels:
app: kotlin-order-service
spec:
containers:
- name: app
image: ghcr.io/myorg/kotlin-order-service:v2.4.1
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0"
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2000m"
memory: "2Gi"
ports:
- containerPort: 8080 Defining the HPA manifest
For most HTTP-based Kotlin APIs, CPU utilization remains the most reliable scaling signal. Memory is often misleading due to JVM heap caching and lazy class loading. Set the target utilization below 80% to provide headroom for new pods joining the load balancer and completing their warm-up phase.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: kotlin-order-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: kotlin-order-service
minReplicas: 2
maxReplicas: 12
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Pods
value: 2
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 120
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 The behavior block above is critical for Kotlin. The 60-second scale-up stabilization prevents thrashing during brief traffic spikes, while the 300-second scale-down window avoids premature termination of pods that have just finished warming up their JIT compiler and connection pools. This asymmetric behavior matches the actual cost profile of JVM restarts.
When should you use custom metrics instead of CPU for Kotlin scaling?
CPU utilization works well for synchronous REST APIs, but fails for event-driven Kotlin services processing Kafka messages, RabbitMQ tasks, or gRPC streams. These workloads can be CPU-idle while accumulating massive backlogs. In such cases, you need custom metrics exposed via Micrometer and consumed by the Prometheus Adapter. Understanding the broader observability stack is essential here; see metrics, logs, and traces compared for context on selecting the right signal.
Exposing business metrics from Kotlin
Add the Micrometer Prometheus registry to your Kotlin service. Expose metrics that directly correlate with user experience or system health, such as active thread pool usage, queue consumer lag, or request queue depth.
@Bean
fun meterRegistry(): MeterRegistry {
return PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
}
// In your Kafka consumer or worker component
@Component
class OrderProcessor(private val registry: MeterRegistry) {
private val backlogGauge = registry.gauge("order_processing_backlog", AtomicInteger(0))
fun processBatch(messages: List<ConsumerRecord<String, Order>>) {
backlogGauge.set(messages.size)
// processing logic...
}
} Configuring the Prometheus Adapter
The Prometheus Adapter translates PromQL queries into the Kubernetes Custom Metrics API. Create a ConfigMap that maps your Kotlin-specific metric to an HPA-consumable format. This step is where many teams struggle, as the adapter's rule syntax is unforgiving.
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-adapter-config
namespace: monitoring
data:
config.yaml: |
rules:
- seriesQuery: 'order_processing_backlog{namespace!="",pod!=""}'
resources:
overrides:
namespace: {resource: "namespace"}
pod: {resource: "pod"}
name:
matches: "^(.*)"
as: "kotlin_order_backlog"
metricsQuery: 'avg(order_processing_backlog{<<.ResourceMatchers>>})' Once applied, verify the metric is visible via kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/kotlin_order_backlog". Only after confirming this endpoint returns valid data should you reference it in your HPA.
How does KEDA improve event-driven Kotlin autoscaling?
While the Prometheus Adapter enables custom metrics, it still operates on a polling interval and requires manual PromQL configuration. KEDA (Kubernetes Event-Driven Autoscaling) solves this for event-driven Kotlin services by scaling directly from external event sources. Instead of waiting for Prometheus to scrape and the adapter to translate, KEDA queries Kafka consumer groups, Redis lists, or Azure Service Bus queues directly and scales pods proportionally to pending work.
KEDA ScaledObject for Kafka consumers
For a Kotlin service consuming from Kafka, KEDA can scale from zero to N replicas based on consumer group lag. This is fundamentally different from HPA, which cannot scale to zero and reacts only after metrics are already elevated.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: kotlin-order-consumer-scaler
spec:
scaleTargetRef:
name: kotlin-order-consumer
minReplicaCount: 0
maxReplicaCount: 10
pollingInterval: 15
cooldownPeriod: 120
triggers:
- type: kafka
metadata:
bootstrapServers: kafka-broker.kafka.svc:9092
consumerGroup: order-processor-group
topic: orders.raw
lagThreshold: "100"
offsetResetPolicy: latest The lagThreshold of 100 means KEDA targets approximately 100 unprocessed messages per replica. If lag reaches 800, KEDA requests 8 replicas. This direct mapping between business backlog and compute capacity is impossible with pure CPU-based HPA. Note that KEDA complements rather than replaces HPA; you can run both simultaneously, letting KEDA handle baseline event-driven scaling and HPA handle unexpected CPU spikes during processing bursts.
What are the key differences between HPA, VPA, and KEDA for Kotlin?
Choosing the wrong scaler is the most frequent cause of scaling failures in JVM workloads. Each mechanism serves a distinct purpose, and understanding their trade-offs prevents over-engineering or under-provisioning.
| Scaler | Best For | Kotlin-Specific Caveat | Scale-to-Zero |
|---|---|---|---|
| HPA | Synchronous HTTP APIs, predictable traffic patterns | Requires warm-up buffer; CPU lags behind actual load during GC pauses | No |
| VPA | Right-sizing requests/limits during development or stable-state optimization | Restarts pods to apply recommendations; disruptive for stateful Kotlin sessions | No |
| KEDA | Event-driven consumers, batch processors, queue workers | External trigger latency adds ~15s delay; not suitable for sub-second SLAs | Yes |
| HPA + KEDA | Hybrid workloads (API + async processing in same service) | Complex debugging; ensure metrics don't conflict during simultaneous evaluation | Partial |
In practice, most production Kotlin services benefit from HPA as the primary scaler with KEDA added only when clear event-driven backlogs exist. VPA should be used cautiously in production JVM environments because its recommendation application requires pod restarts, which interrupts JIT compilation caches and connection pool initialization. Reserve VPA for staging environments or maintenance windows where temporary disruption is acceptable.
How do you validate and troubleshoot Kotlin autoscaling in production?
Configuration alone doesn't guarantee correct scaling. You must validate behavior under realistic conditions before trusting autoscaling in production. A common failure mode is discovering during an incident that the HPA never triggered because resource requests were misaligned with actual JVM consumption.
- Verify metrics availability first. Run
kubectl top podsand confirm Metrics Server returns data. Then check custom metrics endpoints if using Prometheus Adapter. Missing metrics silently disable HPA without error messages. - Load test with realistic JVM warm-up. Use tools like k6 or Gatling to simulate traffic ramps over 5–10 minutes, not instant spikes. Kotlin services need time to compile hot paths and stabilize GC. Instantaneous load tests produce misleading scaling thresholds.
- Monitor scaling events alongside application logs. Correlate HPA events (
kubectl get events | grep HPA) with application startup logs. If new pods take 90 seconds to become ready but HPA expects 30-second readiness, you'll see cascading failures during scale-up. AdjustreadinessProbeinitialDelaySeconds accordingly. - Audit resource utilization post-scaling. After a scaling event, check whether new pods actually reduced per-pod CPU/memory pressure. If utilization remains high despite adding replicas, your bottleneck may be downstream (database connections, external APIs) rather than compute. Scaling more pods won't help and may worsen the situation.
- Implement circuit breakers before enabling aggressive scaling. Autoscaling amplifies downstream load. Without resilience patterns, scaling 10x can overwhelm databases or third-party services. See circuit breakers and resilience patterns for implementation guidance specific to Kotlin coroutines and Spring Cloud CircuitBreaker.
Additionally, enable HPA dry-run mode in non-production clusters to observe scaling decisions without actually modifying replica counts. This validates your metric thresholds and behavior policies safely. In 2026, most managed Kubernetes platforms also offer scaling simulation tools in their console—use them before committing to production configurations.
Production checklist for Kotlin autoscaling
Successfully operating autoscaled Kotlin services requires ongoing validation, not just initial configuration. Treat your scaling setup as code that needs testing, monitoring, and periodic review. Start with conservative thresholds and tighten them based on observed production behavior over several weeks. Document your scaling rationale in your repository alongside the manifests—future engineers (including yourself at 3 AM) will thank you.
If you're implementing autoscaling for a Kotlin service and need hands-on guidance tailored to your specific architecture, compliance requirements, or team maturity level, reach out for a consultation. I help teams build production-grade Kubernetes platforms that scale safely and pass audits confidently.