
Table of Contents
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.
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-clientlibrary; values above 100ms indicate saturation regardless of CPU.
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.
| Criteria | Native HPA + Prometheus Adapter | KEDA |
|---|---|---|
| Best workload type | HTTP APIs, synchronous services | Queue consumers, event processors, cron jobs |
| External trigger support | Via Prometheus exporters only | Native: SQS, Kafka, Redis, Azure, GCP, etc. |
| Scale-to-zero | No (minReplicas ≥ 1) | Yes (scaledObject.minReplicaCount: 0) |
| Operational complexity | Moderate (Prometheus + Adapter) | Higher (KEDA operator + trigger auth) |
| Metric source flexibility | Any PromQL query | Pre-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.
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
- HPA MaxReplicas Reached: Fires when desired replicas equal maxReplicas for more than 5 minutes. Indicates either undersized max or runaway load.
- Scale-Up Velocity Exceeded: Tracks when the HPA wants to add pods faster than your policy allows. Signals potential burst underprotection.
- Pod Unready During Scale-Up: New pods failing readiness probes during scaling events. Points to slow startup, missing health checks, or dependency exhaustion.
- 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.