Nov 6, 20267 min read

Kubernetes HPA: Horizontal Pod Autoscaling for Backends

How Horizontal Pod Autoscaler metrics, min and max replicas and scale behavior work, when HPA fails on cold starts and database limits, and how Encore scales services in your AWS or GCP account without HPA YAML in the app.

The Kubernetes Horizontal Pod Autoscaler (HPA) changes how many pods a Deployment runs so capacity tracks demand. For backends that already sit on a cluster, it is the usual way to grow and shrink the API and worker tiers from live metrics.

Getting the YAML right still leaves the harder part: metrics have to match how the service actually saturates, min and max replicas have to leave room for rollouts and cold starts, and the database has to survive the extra connections when scale-out succeeds. This guide covers those pieces and how Encore scales services in your own AWS or GCP account from infrastructure declared in application code.

HPA at a glance

SettingWhat it doesWhat to watch
scaleTargetRefThe Deployment (or similar) whose replica count HPA ownsDo not also hard-code spec.replicas in the same manifest
minReplicasFloor for replica count (default 1)Keep enough pods for rollouts and sudden spikes
maxReplicasCeiling HPA will not exceedMust be at least minReplicas
Resource metricsCPU or memory vs requestsContainers need resource requests set
Custom / external metricsQueue depth, RPS, business signalsNeeds a metrics adapter in the cluster
behaviorScale-up and scale-down rate limits and windowsDefault downscale stabilization is about five minutes

How HPA chooses a replica count

HPA runs as a control loop in the control plane, by default about every fifteen seconds. It reads the metrics listed on the HorizontalPodAutoscaler, computes a desired replica count for each metric, and picks the largest of those recommendations, then clamps the result between minReplicas and maxReplicas. The core ratio is current metric value over desired metric value, so if average CPU is twice the target, the controller aims to roughly double the pods.

Resource utilization metrics need container resource requests on the pods. Without requests, CPU utilization is undefined and HPA will not act on that metric. The autoscaling/v2 API is the stable surface for memory, multiple metrics and configurable behavior such as scale-up policies and a downscale stabilization window.

For backends, CPU alone is often a poor signal because a service blocked on Postgres or an HTTP client can look idle on CPU while latency climbs. Custom or external metrics (request rate, queue depth, p95 latency) fit those cases once a metrics pipeline exposes them to the custom or external metrics APIs.

Min and max replicas in practice

minReplicas is the count you keep running when traffic is quiet. Setting it to 1 saves money and leaves no spare capacity during a rolling update or a bad deploy. Many production APIs keep a floor of two or three across zones so one pod failure or one surge does not take the service dark. Scaling all the way to zero is a separate path: with recent Kubernetes it is supported for object or external metrics, not for CPU or memory resource metrics, because those can only be measured on running pods.

maxReplicas is the safety rail. Without a ceiling, a bad metric or a retry storm can schedule more pods than the cluster or the database can feed. The max should reflect node capacity, Pod Disruption Budgets and the hard limits of shared dependencies, not only the budget for compute.

When HPA fails for backends

Cold start and readiness delay are the first failure mode. New pods spend time pulling images, running init containers and passing readiness probes before they accept traffic. HPA also treats recently started pods carefully when scaling on CPU, with an initialization period that defaults to five minutes, which dampens scale-up while pods are still warming. A flash crowd can exhaust the Ready pods before the new ones join.

Database and connection limits are the second failure mode: each new pod opens its own pool. Ten pods with twenty connections each are two hundred sessions on Postgres before migrations and admin tools. Scale-out that ignores max_connections, RDS Proxy or a similar pooler turns a healthy HPA event into connection refused errors. Write-heavy paths still hit one primary, so missing indexes and hot rows show up as soon as the API tier grows.

Wrong metrics and thrashing are the third failure mode, where scaling on CPU while the bottleneck is a downstream API, or oscillating around a noisy metric without a stabilization window, burns churn without fixing latency. HPA also cannot place pods if the cluster is out of CPU, memory or IP addresses; that needs a cluster autoscaler or more node capacity beside the workload autoscaler.

How Encore scales services in your cloud

Encore

Services you declare in Encore are infrastructure from code, and the platform provisions and deploys them into your own AWS or GCP account. You declare services and the databases, Pub/Sub, object storage, caches, cron jobs and secrets they use in code. On deploy, Encore creates the matching managed resources and runs the services on Fargate or Kubernetes in that account, as described in the AWS infrastructure docs, the Kubernetes deploy docs and infrastructure configuration.

The Infrastructure page in the Encore dashboard with a database's configuration panel open, showing fields for vCPUs, memory, storage, availability type, max connections and backups.
Each provisioned resource can be reviewed and its configuration edited per environment in the Encore dashboard. The screenshot shows a GCP Cloud SQL database.

Encore is not a Kubernetes distribution: when you choose Kubernetes for an environment it deploys each service as a Deployment on GKE or EKS Fargate, and when you choose Fargate or Cloud Run it runs containers without you maintaining cluster manifests. Process allocation decides whether services share one process or each get their own, so a hot API can scale independently of a quieter worker. You review compute and database settings per environment in the dashboard, and Encore provisions an IAM role for the service.

Set up the app

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

~
$ encore app create checkout --example=ts/empty$ cd checkout && mkdir checkout

A service that keeps durable state in Postgres and offloads slow work to a topic stays safe when replica counts rise, because any healthy instance can handle the next request:

// checkout/encore.service.ts import { Service } from "encore.dev/service"; export default new Service("checkout");
// checkout/checkout.ts import { api } from "encore.dev/api"; import { Topic } from "encore.dev/pubsub"; import { SQLDatabase } from "encore.dev/storage/sqldb"; const db = new SQLDatabase("checkout", { migrations: "./migrations" }); export interface OrderPlaced { orderId: string; } export const orders = new Topic<OrderPlaced>("orders", { deliveryGuarantee: "at-least-once", }); export const place = api( { expose: true, method: "POST", path: "/orders" }, async ({ sku }: { sku: string }): Promise<{ orderId: string }> => { const row = await db.queryRow<{ id: string }>` INSERT INTO orders (sku) VALUES (${sku}) RETURNING id::text AS id`; const orderId = row!.id; await orders.publish({ orderId }); return { orderId }; }, );
-- checkout/migrations/1_create_orders.up.sql CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, sku TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() );

encore run starts Postgres locally and runs the service against a local Pub/Sub implementation. After you connect your cloud account, a push deploys the same code with managed Postgres and messaging in your account. The TypeScript database docs and Pub/Sub docs cover the APIs.

An AI coding agent can add a second service or a subscription in the same change, run it with encore run, and leave replica and database sizing to the environment configuration rather than authoring HorizontalPodAutoscaler manifests next to the handler.

Best fit: Teams who want backends that scale as services in their own AWS or GCP account, with infrastructure from code and shared managed databases, queues and storage.

Good to know

  • Set resource requests before trusting CPU or memory utilization targets.
  • Raise database connection budgets and pooling before raising maxReplicas.
  • Prefer request, queue or latency metrics when the bottleneck is I/O rather than CPU.

Frequently asked questions

What is Kubernetes HPA?

The Horizontal Pod Autoscaler is a Kubernetes controller that raises or lowers the replica count of a Deployment or similar workload to match demand. It reads metrics such as average CPU utilization, compares them to a target, and sets desired replicas between a configured minimum and maximum.

What metrics does HPA use?

Resource metrics such as CPU and memory utilization are the common starting point and need requests set on containers. The autoscaling/v2 API also supports container resource metrics, custom metrics and external metrics, and when several metrics are set HPA takes the highest recommended replica count.

Why does HPA fail during traffic spikes?

New pods need time to pull images, pass readiness probes and warm up before they take traffic, so a sudden spike can overload the pods that are already Ready. Scaling the API tier without headroom on the database connection limit or write capacity moves the outage one layer down.

How do I scale a backend in AWS or GCP without writing HPA YAML?

With Encore, declare services and their databases, queues and storage in TypeScript or Go, connect your AWS or GCP account, and choose Fargate or Kubernetes as the compute for an environment. Encore deploys each service as a unit in your account and you adjust process allocation and resource settings per environment in the dashboard.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders