Autoscale a Node.js Service on Kubernetes

Khimananda Oli 8 min read Programming and Languages
Autoscale a Node.js Service on Kubernetes

By Khimananda Oli | Last reviewed: August 2026

To successfully autoscale a Node.js service on Kubernetes, you must move beyond default CPU-based Horizontal Pod Autoscaling (HPA) and align your scaling triggers with the actual event-driven nature of JavaScript runtimes. Node.js is single-threaded and often I/O-bound, meaning CPU utilization frequently remains low even when the application is saturated with pending requests or message queue backpressure. In this guide, I will walk you through configuring production-grade autoscaling that responds to real workload signals like HTTP request rate or active connections, ensuring your service handles traffic spikes without over-provisioning during quiet periods.

Node.js PodsApp + /metricsMetrics ServerCPU / MemoryPrometheus AdapterCustom Metrics APIHPA ControllerScaling LogicDeploymentReplica SetAutoscale a Node.js Service on Kubernetes: Control Plane Flow
Control plane components required to autoscale a Node.js service on Kubernetes reliably

How do you configure resource limits before you autoscale a Node.js service on Kubernetes?

Before you create a single HPA manifest, you must establish accurate resource baselines. A common mistake I see in audits across Nepal and global teams is enabling autoscaling on containers with missing or copy-pasted resource requests. The HPA calculates replica counts as a ratio of current-to-target utilization; if your requests are wrong, your scaling math is wrong. For Node.js specifically, memory is usually the binding constraint because V8 heap grows with concurrent connections and cached objects, while CPU can remain deceptively low during I/O waits.

Profiling your Node.js container

Run a representative load test against a single pod with kubectl top pods and Prometheus metrics active. Record the p95 memory and CPU under sustained peak load. Set your requests to the p50–p75 baseline to ensure bin-packing efficiency, and set limits to at least 1.5× the p95 observed peak to accommodate garbage collection spikes without OOMKill events. Never set memory limits equal to requests for Node.js; the runtime needs headroom for GC cycles.

<!-- deployment.yaml excerpt -->
resources:
  requests:
    cpu: "250m"
    memory: "384Mi"
  limits:
    cpu: "1000m"
    memory: "768Mi"

If you are unsure where to start, consult my guide on Kubernetes resource limits and requests for a deeper methodology on profiling async runtimes. Accurate requests are the foundation upon which all effective autoscaling depends.

Why should you use custom metrics to autoscale a Node.js service on Kubernetes instead of CPU?

CPU-based HPA works adequately for compute-bound Go or Java services, but it fails silently for Node.js APIs and workers. Because Node.js uses an event loop, a pod can be completely saturated with thousands of pending database queries or HTTP calls while reporting only 30% CPU usage. By the time CPU crosses the 70% threshold, latency has already spiked and users are experiencing timeouts. Custom metrics close this gap by scaling on what actually matters to your business.

Choosing the right scaling signal

  • HTTP Requests Per Second: Best for REST/GraphQL APIs. Directly correlates with user-facing load.
  • Active Connections: Ideal for WebSocket servers or long-lived streaming endpoints.
  • Queue Depth / Lag: Critical for BullMQ, RabbitMQ, or Kafka consumers where backlog matters more than instantaneous throughput.
  • Event Loop Lag: A Node.js-specific metric exposed by the prom-client library; values above 100ms indicate saturation regardless of CPU.
What is the workload type?REST / GraphQL APIAsync Worker / QueueWebSocket / StreamingScale on:http_requests_per_secondScale on:queue_depth / lagScale on:active_connectionsAlways add event_loop_lag as secondary safety metricMetric selection determines whether you autoscale a Node.js service on Kubernetes effectively or react too late
Select the correct scaling metric based on your Node.js workload pattern

Exposing and wiring custom metrics

Instrument your Node.js app with prom-client to expose a /metrics endpoint. Deploy Prometheus to scrape it, then install the Prometheus Adapter to translate PromQL queries into the Kubernetes Custom Metrics API. Your HPA then references these metrics natively. This stack is now standard in 2026 for any team that needs to autoscale a Node.js service on Kubernetes with meaningful signals.

How do you write an HPA manifest to autoscale a Node.js service on Kubernetes with stabilization?

Flapping — rapid scale-up followed immediately by scale-down — is the most frequent production incident I diagnose with HPA misconfigurations. Node.js pods have non-trivial startup costs: npm dependency loading, JIT warmup, and connection pool establishment can take 15–30 seconds. If your HPA scales down too aggressively, you create a sawtooth pattern that degrades latency worse than static provisioning. Stabilization windows solve this.

# hpa-nodejs-api.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: nodejs-api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: nodejs-api
  minReplicas: 3
  maxReplicas: 30
  metrics:
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: "150"
    - type: Pods
      pods:
        metric:
          name: nodejs_event_loop_lag_seconds
        target:
          type: AverageValue
          averageValue: "0.1"
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
      policies:
        - type: Pods
          value: 4
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Pods
          value: 2
          periodSeconds: 120

The scaleDown.stabilizationWindowSeconds: 300 setting tells Kubernetes to wait five full minutes after the last scale recommendation before removing pods. This absorbs traffic valleys without premature termination. The scaleUp policy caps additions to 4 pods per minute to prevent thundering herd effects on downstream databases. For teams managing multiple environments, integrating this HPA into a GitOps workflow via ArgoCD ensures scaling policies are version-controlled and auditable.

How does KEDA compare to native HPA when you autoscale a Node.js service on Kubernetes?

KEDA (Kubernetes Event-Driven Autoscaling) extends HPA with external trigger sources that the native API cannot reach directly. While native HPA requires the Prometheus Adapter pipeline for custom metrics, KEDA connects natively to AWS SQS, Azure Service Bus, Redis Streams, Kafka, and cron schedules without intermediate adapters. For pure HTTP APIs, native HPA with Prometheus Adapter is simpler and has fewer moving parts. For event-driven workers, KEDA eliminates significant plumbing.

CriteriaNative HPA + Prometheus AdapterKEDA
Best workload typeHTTP APIs, synchronous servicesQueue consumers, event processors, cron jobs
External trigger supportVia Prometheus exporters onlyNative: SQS, Kafka, Redis, Azure, GCP, etc.
Scale-to-zeroNo (minReplicas ≥ 1)Yes (scaledObject.minReplicaCount: 0)
Operational complexityModerate (Prometheus + Adapter)Higher (KEDA operator + trigger auth)
Metric source flexibilityAny PromQL queryPre-built triggers + custom Prometheus fallback
Recommended for Node.js API✅ Yes⚠️ Overkill unless multi-trigger
Recommended for Node.js worker⚠️ Requires exporter setup✅ Yes

In practice, many teams I advise use both: native HPA for their Express/Fastify API tier and KEDA for their BullMQ or Kafka consumer deployments. This hybrid approach lets you autoscale a Node.js service on Kubernetes with the right tool for each workload shape rather than forcing one abstraction across fundamentally different scaling dynamics.

Native HPA PathNode.js /metricsPrometheusPrometheus AdapterCustom Metrics APIHPA ControllerKEDA PathExternal SourceKEDA OperatorScaledObject CRDHPA (generated)Choose the right path when you autoscale a Node.js service on Kubernetes
Native HPA versus KEDA architecture comparison for Node.js autoscaling decisions

What monitoring validates that autoscaling is working correctly for Node.js on Kubernetes?

Deploying an HPA is not the finish line; validating its behavior under real conditions is. You need observability into three dimensions simultaneously: the scaling controller's decisions, the application's health during transitions, and the underlying infrastructure capacity. Without this triad, you will debug scaling incidents blindly. I recommend building a dedicated Grafana dashboard that overlays HPA replica count, target metric value, actual metric value, and pod readiness state on a single timeline. This immediately reveals whether scale-ups are timely and scale-downs are safe.

Critical alerts to configure

  1. HPA MaxReplicas Reached: Fires when desired replicas equal maxReplicas for more than 5 minutes. Indicates either undersized max or runaway load.
  2. Scale-Up Velocity Exceeded: Tracks when the HPA wants to add pods faster than your policy allows. Signals potential burst underprotection.
  3. Pod Unready During Scale-Up: New pods failing readiness probes during scaling events. Points to slow startup, missing health checks, or dependency exhaustion.
  4. Metric Staleness: Custom metric absent from Prometheus for >2 minutes. The HPA freezes on stale data and stops scaling entirely.

For comprehensive instrumentation guidance, see my article on instrumenting applications with OpenTelemetry, which covers adding autoscaling-relevant traces and metrics to Node.js services. Pair this with the four golden signals framework to ensure your scaling dashboards reflect actual user experience, not just infrastructure counters.

Conclusion

To reliably autoscale a Node.js service on Kubernetes in 2026, you need accurate resource baselines, workload-appropriate custom metrics, stabilization windows tuned to your startup characteristics, and continuous validation through targeted observability. Skip any of these steps and your autoscaler becomes a liability during the exact traffic events it was designed to handle. Start by profiling a single pod under load, choose your scaling signal deliberately, and treat your HPA configuration as production code that deserves testing, review, and iteration. If your team needs help designing or auditing a Node.js autoscaling strategy that survives real-world traffic and compliance reviews, reach out to discuss your infrastructure.

Frequently Asked Questions

Apply a HorizontalPodAutoscaler manifest targeting your Node.js Deployment with cpu or memory metrics. Ensure the metrics-server is installed and your pods expose resource requests. Use kubectl autoscale deployment node-app --cpu-percent=70 --min=2 --max=10 for quick setup in 2026 clusters.

CPU utilization works for compute-bound tasks, but custom metrics like event loop lag or active HTTP connections better reflect Node.js concurrency limits. Use Prometheus Adapter to expose application-specific metrics to HPA, preventing scaling delays caused by garbage collection pauses masking true load in V8 runtime environments.

Default HPA sync periods are fifteen seconds. Reduce --horizontal-pod-autoscaler-sync-period to five seconds and configure behavior.scaleUp.policies with higher replica increments. Node.js cold starts add latency, so pre-warm containers or use KEDA for event-driven scaling that reacts faster than metric averaging allows.

Yes. Expose event loop delay via prom-client, scrape with Prometheus, and configure Prometheus Adapter to map it to a custom HPA metric. This captures actual blocking time better than CPU usage, which often stays low during I/O waits despite degraded request latency in single-threaded Node processes.

Set CPU requests to 250m and memory to 512Mi for typical Express apps. Profile production workloads first using kubectl top and V8 heap snapshots. Overestimating prevents throttling; underestimating triggers excessive scaling. Align limits with Node.js max-old-space-size to avoid OOM kills during garbage collection spikes.

Configure stabilizationWindowSeconds in HPA behavior for both scale-up and scale-down. Set scale-down window to at least 300 seconds to absorb traffic dips. Add tolerance thresholds to metrics and ensure adequate pod disruption budgets. Flapping often stems from aggressive defaults ignoring Node.js warmup and connection draining times.

VPA in Auto mode conflicts because it restarts pods to apply recommendations, disrupting HPA stability. Use VPA in Off or Initial mode to right-size requests once, then let HPA handle horizontal scaling. Never run both controllers in reactive mode simultaneously on the same Node.js workload in production.

Use k6 or Artillery to generate realistic load against a staging cluster with identical HPA config. Monitor scaling events via kubectl get hpa -w and verify metrics accuracy. Validate that scale-down respects graceful shutdown hooks and that new pods pass readiness probes before receiving traffic during peak simulation.

Use Prometheus trigger for custom app metrics, RabbitMQ or Kafka trigger for queue-based workers, and HTTP trigger for serverless-style scaling. KEDA scales from zero, unlike HPA, making it ideal for sporadic Node.js jobs. Define scaledObject with pollingInterval matching your metric resolution to avoid lag.

Cluster Autoscaler adds nodes when HPA cannot schedule new pods due to insufficient resources. Ensure node groups have appropriate instance types matching Node.js memory profiles. Configure expander strategies to prefer cost-efficient nodes. Delays occur if node provisioning exceeds pod startup time, creating temporary capacity gaps during traffic spikes.

New pods may load large caches or initialize heavy dependencies concurrently. Stagger rollouts using maxSurge and implement lazy initialization. Check if NODE_OPTIONS includes appropriate max-old-space-size relative to container memory limit. Memory leaks exposed only under rapid scaling indicate missing cleanup in global state or event listeners.

Restrict RBAC permissions for HPA and metrics endpoints. Validate SLOs before exposing custom metrics. Encrypt metric pipelines with mTLS. Avoid scaling based on untrusted external inputs that could trigger denial-of-wallet attacks. Audit HPA events regularly and set maxReplicas caps aligned with budget and infrastructure quotas.

Costs scale linearly with replica count and node provisioning. Right-sized requests prevent overprovisioning; efficient scale-down policies reduce idle spend. Spot instances cut costs for stateless Node.js workers. Monitor cost-per-request metrics alongside HPA activity. Unoptimized scaling can triple bills during transient spikes without delivering proportional throughput gains.

Run kubectl describe hpa to check conditions and events. Verify metrics-server returns data via kubectl top pods. Confirm resource requests are set on all containers. Check if custom metrics adapter is healthy. Common issues include missing labels, stale metrics, or misconfigured targetUtilization values exceeding actual observable ranges.

Use HPA for steady, predictable traffic with CPU/memory correlation. Choose KEDA for bursty APIs needing sub-second reaction, scale-to-zero, or multi-metric logic. KEDA adds complexity but handles edge cases HPA cannot. Many teams run HPA as baseline safety net with KEDA managing primary scaling signals for gateway workloads.