Autoscale a Kotlin Service on Kubernetes

Khimananda Oli 9 min read Programming and Languages
Autoscale a Kotlin Service on Kubernetes

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.

Kotlin PodJVM + Micrometer/actuator/prometheusMetrics ServerCPU / MemoryPrometheus AdapterCustom Metrics APIHPA ControllerScale DecisionReplicaSet Update
Control plane components required to autoscale a Kotlin service on Kubernetes with both standard and custom metrics

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.

Kotlin ServiceMicrometer Gaugebacklog=142PrometheusScrape /storeTime-series DBProm AdapterPromQL → K8s APIcustom.metricsHPA ControllerEvaluate TargetAdjust Replicas
Data flow for custom metrics when you autoscale a Kotlin service on Kubernetes using Micrometer and Prometheus Adapter

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.

ScalerBest ForKotlin-Specific CaveatScale-to-Zero
HPASynchronous HTTP APIs, predictable traffic patternsRequires warm-up buffer; CPU lags behind actual load during GC pausesNo
VPARight-sizing requests/limits during development or stable-state optimizationRestarts pods to apply recommendations; disruptive for stateful Kotlin sessionsNo
KEDAEvent-driven consumers, batch processors, queue workersExternal trigger latency adds ~15s delay; not suitable for sub-second SLAsYes
HPA + KEDAHybrid workloads (API + async processing in same service)Complex debugging; ensure metrics don't conflict during simultaneous evaluationPartial

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.

  1. Verify metrics availability first. Run kubectl top pods and confirm Metrics Server returns data. Then check custom metrics endpoints if using Prometheus Adapter. Missing metrics silently disable HPA without error messages.
  2. 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.
  3. 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. Adjust readinessProbe initialDelaySeconds accordingly.
  4. 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.
  5. 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.

Workload Type?Sync HTTP API→ HPA (CPU 70%)Event Consumer→ KEDA (Lag-based)Hybrid Workload→ HPA + KEDAAdd StabilizationscaleDown: 300sSet Min Replicas ≥1Avoid cold-start lagMonitor ConflictsSeparate metrics
Decision framework for choosing the right scaler when you autoscale a Kotlin service on Kubernetes

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.

Frequently Asked Questions

Deploy metrics-server, define resource requests and limits in your Kotlin Deployment manifest, then create a HorizontalPodAutoscaler targeting CPU or custom metrics with kubectl apply.

Yes, use JVM heap usage or request latency via Prometheus Adapter instead of raw CPU for accurate Kotlin coroutine workload scaling.

Slow JVM startup delays readiness; configure startup probes, use CDS/AppCDS class data sharing, and set appropriate stabilization windows in HPA spec.

KEDA extends HPA with event-driven triggers like Kafka lag or queue depth, offering finer granularity than standard metrics-based autoscaling for reactive Kotlin services.

Set scaleUp and scaleDown stabilization windows to 300 seconds minimum and use behavior policies to dampen rapid replica count changes during traffic spikes.

Profile production JVM memory with NativeMemoryTracking, set requests to p95 baseline usage, and limits 20 percent higher to avoid OOMKills during GC pauses.

Native images reduce cold start from seconds to milliseconds, enabling faster scale-out response, but require ahead-of-time compilation and reflection configuration tuning.

Use k6 or Locust to generate synthetic load against staging cluster while monitoring HPA events with kubectl get hpa -w to validate scaling thresholds.

Metrics-server cannot reach kubelet metrics endpoint; verify RBAC permissions, check metrics-server logs, and ensure resource requests are defined in Kotlin deployment.

Avoid combining VPA and HPA on same resource metric; use VPA in recommendation mode only to right-size requests, letting HPA handle replica scaling.

Oversized dispatchers mask true concurrency; align dispatcher threads with pod CPU limits so HPA sees actual saturation rather than idle thread pool capacity.

Unvalidated custom metrics endpoints can be exploited; secure Prometheus Adapter with mTLS, restrict RBAC to HPA namespace, and audit metric source authenticity.

Multiply max replicas by pod resource cost per hour, factor in average utilization patterns from monitoring dashboards, and include node provisioning buffer expenses.

Expose custom metrics via Micrometer, push to Prometheus, configure Prometheus Adapter mapping rules, then reference external metric name in HPA spec.

HPA maintains last known replica count without scaling; deploy metrics-server with high availability, multiple replicas, and pod disruption budgets to prevent outages.