Custom Metrics Autoscaling in Kubernetes
CPU alone cannot tell your cluster how busy your application really is. Learn how Kubernetes can grow and shrink your pods based on requests per second, queue depth, consumer lag and any other signal that matters to your business.
Imagine a pizza shop with a single oven and a fixed team of five cooks. On a quiet Tuesday afternoon, five cooks are far too many, and you are paying wages for people standing around. On a Friday night, with orders flooding in, five cooks are far too few, and customers walk out angry. A smart manager does not stare at how tired the cooks look. The manager looks at the number of unfinished orders on the wall and calls in more cooks when that number grows.
Kubernetes applications face the same problem every day. Traffic goes up and down, and the number of running copies of your application (called pods) should follow. Kubernetes has a built-in tool for this, and by default it watches how hard the processor (CPU) is working. That is a bit like judging the pizza shop by how tired the cooks look. It sometimes works, and it often fails badly.
This guide teaches you how to make Kubernetes watch the wall of unfinished orders instead. That is what custom metrics autoscaling means: telling Kubernetes to add or remove pods based on numbers that come from your own application or from systems around it, such as requests per second, messages waiting in a queue, or how far a consumer has fallen behind. We will start from zero, explain every new word the first time it appears, and build up to the details that production engineers worry about at three in the morning.
Why CPU Alone Is Not Enough
Before learning the tool, we need to understand the problem it solves and why the default answer is often wrong.
What is autoscaling?
Autoscaling means that a system changes its own size automatically to match the work it has to do. When there is a lot of work, it adds more workers. When the work disappears, it removes the extra workers so you stop paying for them. In Kubernetes, the “workers” are usually pods, and a pod is the smallest unit you can run: one or more containers that live and die together.
Autoscaling matters for two reasons. The first is reliability: if traffic doubles and you do nothing, your service slows down or crashes. The second is cost: if you always run enough pods for your busiest day, you waste money on the other 364 days. Good autoscaling gives you both safety and savings.
Think of supermarket checkout lanes. When the queue at every lane gets long, the store opens more lanes. When the store empties, lanes close and the cashiers go back to stocking shelves. The store does not decide based on how sweaty the cashiers are. It decides based on how many customers are waiting.
The default: scaling on CPU
The original Kubernetes autoscaler, the Horizontal Pod Autoscaler (HPA), could only look at CPU usage in its earliest version. If your pods averaged more than, say, 70 percent of the CPU they asked for, the HPA added pods. If usage fell, it removed some. For many simple web applications that do a lot of calculation per request, this is a reasonable rule.
The word horizontal matters. Scaling horizontally means adding more copies of the same thing. Scaling vertically means making one copy bigger, for example giving a pod more CPU. Kubernetes can do both, but horizontal scaling is the more common tool because it also improves availability: more copies means one failure hurts less.
Where CPU lies to you
CPU is a proxy, meaning a stand-in for the thing you really care about. What you really care about is whether users are getting fast, correct answers. CPU only tells you about that when the work is CPU-bound. Many real systems are not.
Pods are busy but not on the CPU
A service that mostly waits for a database reply can have 5 percent CPU and still be completely overloaded. Adding pods based on CPU never triggers.
The backlog is the real signal
A worker that reads jobs from a queue may sleep when the queue is empty and sprint when it is full, but ten thousand waiting jobs is the number that matters, not the CPU.
Memory never goes back down
Many runtimes hold on to memory they have already used. Scaling on memory can keep pods alive long after the traffic is gone.
Work arrives in bursts
A stream consumer may be idle for an hour and then receive a million events. By the time CPU rises, users have already waited.
What custom metrics change
A metric is simply a number that is measured over time, like a thermometer reading. A custom metric is any metric that is not CPU or memory, usually one your own application publishes. Examples include “requests handled per second”, “jobs waiting in the queue”, “active user sessions” or “frames waiting to be rendered”.
When Kubernetes can scale on those numbers, your scaling rule becomes close to a sentence a human would say: “Keep about 100 requests per second on each pod” or “Never let more than 50 jobs wait for each worker”. That rule is easier to reason about, easier to test, and far closer to what users experience.
Scale on the signal that best predicts how unhappy users are about to become, not on the signal that is easiest to measure.
The Autoscaling Family in Kubernetes
Kubernetes has several autoscalers that solve different problems. Knowing which is which prevents a lot of confusion later.
People often say “Kubernetes autoscaling” as if it were one thing. In reality there are at least four tools, and a healthy production setup usually uses more than one of them at the same time. Each one answers a different question.
| Tool | Question it answers | What it changes |
|---|---|---|
| Horizontal Pod Autoscaler (HPA) | How many copies of this application should run? | The replica count of a Deployment or similar |
| Vertical Pod Autoscaler (VPA) | How much CPU and memory should each copy ask for? | The resource requests of pods |
| Cluster Autoscaler or Karpenter | How many machines does the cluster need? | The number of nodes |
| KEDA | How should events and external systems drive scaling, even down to zero? | Creates and feeds an HPA, and handles scale to zero |
How the layers fit together
The easiest way to remember the relationship is to think of a restaurant again. The HPA decides how many waiters are on the floor. The node autoscaler decides how big the building is. Waiters need a place to stand, so when the HPA wants more waiters than the building can hold, the building must grow.
In practice this is exactly what happens. The HPA creates new pods. If no machine has room for them, they sit in a Pending state, which is Kubernetes saying “I have no place to put this yet”. The Cluster Autoscaler, or a newer tool called Karpenter, sees the pending pods and starts new machines. Once the machines are ready, the pods land on them and begin serving traffic. Custom metrics only influence the first step of this chain, but the whole chain determines how quickly you really get capacity.
A hotel has rooms (nodes) and guests (pods). The HPA is the booking desk that decides how many guests to invite based on demand. The node autoscaler is the construction crew that adds a new floor if the rooms run out. If guests are invited faster than the crew can build, some of them wait in the lobby.
A short history
The HPA first appeared in the early days of Kubernetes and could only use CPU. Later versions added memory and then the ability to read custom metrics, first in beta versions of the API and eventually in the stable autoscaling/v2 API that reached general availability in Kubernetes 1.23. Around version 1.18, a behavior section was added so engineers could control how fast scaling happens in each direction, which solved many “flapping” problems we will discuss later.
In 2019, Microsoft and Red Hat created KEDA (Kubernetes Event-driven Autoscaling) to make it easy to scale on external event sources. It joined the Cloud Native Computing Foundation (CNCF) and reached the top “graduated” maturity level in 2023, which signals that it is widely used and well governed.
Do not let the HPA and the VPA both react to CPU or memory for the same workload. They will fight: one adds pods while the other resizes them, and the result is unpredictable. Using the HPA on a custom metric while the VPA manages CPU and memory requests is a safer pairing.
How the HPA Makes a Decision
At its heart the HPA is a small piece of arithmetic repeated forever. Once you understand the arithmetic, every behavior makes sense.
The control loop
The HPA is not a separate program you install. It is a controller that lives inside the Kubernetes control plane, in a component called the controller manager. A controller is a loop that repeatedly compares the world as it is with the world as you asked for, and then takes action to close the gap. The HPA loop wakes up about every 15 seconds. Each time, it reads the current metric values, does the arithmetic, and decides whether to change the number of replicas.
You describe what you want in an object called a HorizontalPodAutoscaler. It contains four main ideas: which workload to scale (the target), the smallest and largest number of replicas allowed, which metrics to watch, and what value of each metric counts as “just right”.
The core formula
The formula is short enough to remember:
desired replicas = ceiling of (current replicas × current metric value ÷ target metric value)
The “ceiling” means round up to the next whole number, because you cannot run 5.3 pods. Let us walk through two examples with plain numbers.
Web service scaled on requests per second
You run 4 pods and you want about 100 requests per second on each one. Right now each pod is handling 150. The ratio is 150 ÷ 100 = 1.5. Multiply by 4 pods to get 6. The HPA scales to 6 pods, which brings each pod back down to about 100.
Queue workers scaled on backlog
You run 4 workers and want at most 100 waiting jobs per worker. The queue holds 1,200 jobs, so the average is 300 per worker. The ratio is 3, and 4 × 3 = 12. The HPA asks for 12 workers.
Traffic falls
Later each of 12 workers sees only 25 waiting jobs. The ratio is 0.25, and 12 × 0.25 = 3. The HPA wants 3 workers, but it will wait before acting, for reasons we cover in the tuning chapter.
Tolerance: ignoring tiny differences
If the HPA reacted to every tiny wiggle, the replica count would jitter constantly. To prevent that, there is a tolerance, which is 10 percent by default. If the ratio of current to target is within 10 percent of 1.0, the HPA does nothing. A reading of 105 against a target of 100 gives a ratio of 1.05, which is inside the tolerance, so nothing changes.
More than one metric
An HPA can watch several metrics at once, such as CPU and requests per second. It performs the calculation separately for each metric and then picks the largest answer. This is a safety-first rule: whichever signal says “I need the most help” wins. The result is then clamped between your configured minimum and maximum replica counts.
Picture a lifeguard watching three things: crowd size, wave height and weather. Each alarm suggests a number of lifeguards needed. The beach manager always staffs for the highest number, because being short-staffed on any one danger is worse than being slightly over-staffed.
Missing data is treated carefully
Sometimes a pod has no metric reading, perhaps because it just started or its metrics endpoint failed. The HPA does not guess optimistically. When deciding to scale up, it pretends the missing pods are using none of the target, which avoids overreacting. When deciding to scale down, it pretends the missing pods are using exactly the full target, which avoids removing capacity based on incomplete information. Pods that are still starting up are also treated specially for a short period so that their startup spike does not fool the autoscaler.
The HPA is proportional, not magical. If your metric does not fall when you add pods, the loop will keep adding pods until it reaches the maximum. Choosing a metric that responds to capacity is the single most important design decision.
The Metric Types and the Three Metric APIs
The HPA does not talk to your database of metrics directly. It talks to the Kubernetes API, and special add-ons fill that API with data.
Five kinds of metric in an HPA
When you write an HPA you choose, for each metric, one of five types. Each type tells the HPA where the number comes from and how to interpret it.
CPU or memory of the pods
The built-in option. Values are averaged across all pods of the target and usually expressed as a percentage of what each pod requested.
CPU or memory of one container
Useful when a pod has a main app plus sidecars, so a noisy helper container does not distort the numbers. It became stable in Kubernetes 1.30.
A custom number measured per pod
Each pod reports a value such as requests per second. The HPA averages them across the pods. The target is always an average per pod.
A number about one other Kubernetes object
For example, the request rate seen by a particular Ingress. It describes a single shared object, not each pod.
A number from outside the cluster
Queue length in a cloud messaging service, or a business number in a monitoring product. It has no connection to any pod.
Three metric APIs
Kubernetes has a feature called the aggregation layer. It lets add-on programs plug extra endpoints into the main Kubernetes API, as though those endpoints were built in. The HPA uses this to ask three different questions through three different API groups.
| API group | Serves | Typically provided by |
|---|---|---|
metrics.k8s.io | Resource metrics: CPU and memory of pods and nodes | Metrics Server |
custom.metrics.k8s.io | Custom metrics tied to Kubernetes objects such as pods | Prometheus Adapter, a cloud vendor adapter, or a monitoring agent |
external.metrics.k8s.io | External metrics not tied to any Kubernetes object | KEDA, Prometheus Adapter, or a monitoring product |
Here is a detail that surprises many newcomers. Kubernetes allows only one provider per API group at a time. That means you cannot run two different programs that both serve custom.metrics.k8s.io. You can, however, run one provider for the custom API and a different provider for the external API, which is common: a Prometheus-based adapter for custom metrics next to KEDA for external ones.
Think of the Kubernetes API as a big reception desk in an office. The desk itself only knows basic facts about the building. For specialist questions, it forwards the visitor to a department: one department for facilities (Metrics Server), one for “things about our own staff” (custom metrics), and one for “news from the outside world” (external metrics). Each department has exactly one manager, so two managers cannot claim the same department.
Understanding the units
Metric values are written as Kubernetes quantities. A small letter “m” means “milli”, or one thousandth. So a value of 500m means 0.5, and 100 means one hundred. You will see this in the HPA status when a rate such as 0.3 requests per second appears as 300m. Beginners often stare at it, thinking something is broken, when it is only a unit.
Value versus average value
When you set a target you can often choose between a total value and an average value per pod. Use the average per pod for anything that grows as you add pods, such as requests per pod. Use a total value only when the number describes a single shared thing, for example one Ingress. Mixing these up is a classic mistake: if you target a total queue length of 100 with a value-type target, the HPA compares the whole number of jobs, not the per-worker share, and your scaling math will not match your intention.
For queue-style workloads, prefer expressing the target as “messages per worker”. It makes the formula behave like a ratio of backlog to capacity, which is what you want to control.
Architecture and Components
A custom metrics pipeline has several moving parts. Seeing them on one map makes the rest of this guide easier to follow.
Let us follow a number from the moment it is born inside your application until it causes a new pod to appear. The diagram below shows a very common setup based on Prometheus, which is a popular open source system that collects and stores metrics.
flowchart LR
U["Users / Clients"] --> LB["Load Balancer / Ingress"]
LB --> APP["Application Pods"]
APP -->|"expose /metrics"| PROM["Prometheus"]
PROM -->|"PromQL query"| ADP["Metrics Adapter"]
ADP -->|"registered APIService"| AGG["Kubernetes API Server + Aggregation Layer"]
HPA["HPA Controller in Controller Manager"] -->|"asks for custom metric"| AGG
HPA -->|"updates replicas"| DEP["Deployment"]
DEP --> APP
APP -.->|"pending pods"| CA["Cluster Autoscaler / Karpenter"]
CA -.->|"adds nodes"| NODES["Worker Nodes"]
Walking through the diagram
Start on the left. Users send traffic through a load balancer or an Ingress (the Kubernetes front door) to your application pods. Each pod keeps counters of what it is doing, such as the total number of requests handled, and publishes them on a web page, conventionally at the path /metrics.
Prometheus visits each pod on a schedule, called the scrape interval, copies the numbers, and stores them with timestamps. Prometheus alone, however, is not part of Kubernetes. The HPA has no idea how to talk to it. That gap is filled by the metrics adapter, a small server that translates between two languages. It asks Prometheus questions in Prometheus’s query language and answers the HPA in the format of the Kubernetes custom metrics API.
The adapter registers itself with the Kubernetes API server using an object called an APIService. From then on, when anyone asks the main API server for a custom metric, the server forwards that question to the adapter. The HPA controller is one such asker. It receives the numbers, applies the formula from the previous chapter, and writes a new replica count to the Deployment.
The adapter is a translator at an international meeting. Prometheus speaks one language, the HPA speaks another. Neither is wrong, and neither needs to learn the other’s language. The translator listens to one, speaks to the other, and the meeting works.
The cast of components
Your application
Counts events, measures things, and exposes them. Without good metrics at the source, nothing downstream can help.
Prometheus or a similar system
Scrapes, stores and answers queries about historical and current values.
Metrics adapter
Exposes the stored numbers through the Kubernetes custom or external metrics API.
HPA controller
Runs the formula every cycle and sets the desired replica count.
Deployment controller
Notices the new replica count and creates or deletes pods through the ReplicaSet beneath it.
Node autoscaler
Ensures there are enough machines for all the pods the HPA wants.
What the HPA actually scales
The HPA does not edit pods directly. It writes to something called the scale subresource of its target. Any Kubernetes object that exposes this small standard interface can be scaled, which includes Deployments, StatefulSets and ReplicaSets, and also custom resources defined by operators. This design is why the HPA is so general: it only needs to know “how many replicas are desired”, and it leaves the details of creating them to the owner of the target.
When an HPA manages a Deployment, you should remove the fixed replica count from your deployment definition in version control. Otherwise each new deployment can reset the replicas to the old fixed number and briefly undo the autoscaler’s work.
The Life of One Scaling Event
A scaling event is not instant. It is a relay race with several handoffs, and each handoff adds delay.
Suppose a marketing email goes out and your traffic doubles within a minute. Let us trace what happens, step by step, and see where time is spent. This is called the data flow and lifecycle of an autoscaling event.
sequenceDiagram
participant App as Application Pods
participant Prom as Prometheus
participant Adp as Metrics Adapter
participant API as API Server
participant HPA as HPA Controller
participant Dep as Deployment
participant Sch as Scheduler and Kubelet
App->>Prom: Metrics scraped every 15 to 30 seconds
HPA->>API: Every 15 seconds, ask for metric
API->>Adp: Forward the request
Adp->>Prom: Run the configured query
Prom-->>Adp: Current rate per pod
Adp-->>API: Metric values
API-->>HPA: Metric values
HPA->>HPA: Apply formula and limits
HPA->>Dep: Set new replica count
Dep->>Sch: Create new pods
Sch->>App: Start containers and pass readiness checks
Traffic rises
Pods handle more requests, and their internal counters climb faster. Nothing outside the pod knows yet.
Prometheus scrapes
At its next visit, Prometheus copies the counters. If it scrapes every 30 seconds, the change can be up to 30 seconds old before it is even stored. Rate calculations use a window of several scrapes, which smooths the number but adds a little more delay.
The adapter answers
When the HPA asks, the adapter runs its query and returns the result. The adapter also keeps a list of which metrics exist and refreshes that list on a schedule, by default about once a minute, which matters mostly when you add a new metric.
The HPA decides
On its next 15-second cycle the HPA reads the value, computes the new replica count, and applies the rules from the behavior settings, which may limit how many pods can be added per period.
Pods are created
The Deployment creates new pods. If there is room on existing nodes they start within seconds. If not, the node autoscaler must first add machines, which can take one to several minutes depending on your cloud and machine type.
Pods become ready
Containers pull images, start up, load caches and pass their readiness probe, a check that tells Kubernetes “I am ready for traffic”. Only then does the Service send requests to them.
The metric falls
With more pods sharing the load, the per-pod number drops toward the target. The loop is closed and the HPA stops adding pods.
Adding up the delay
If you add the pieces, a typical well-tuned system needs about one to two minutes to react end to end, and longer if new machines are required. That is acceptable for gradual traffic growth, but it can be too slow for a sudden spike. Understanding this chain tells you where to invest: shorter scrape intervals and lighter images reduce delay, while a small buffer of spare capacity covers the part you cannot shorten.
Autoscaling is reactive. It always lags behind the event it responds to. Design your service so that it can survive the lag, using queues, rate limits and a sensible minimum number of replicas.
Prometheus Adapter in Depth
The most common way to feed custom metrics to Kubernetes is the Prometheus Adapter. Here is how it thinks.
What it is and why it exists
The Prometheus Adapter is a community project maintained under the Kubernetes special interest groups. Its job is simple to say: take numbers stored in Prometheus and serve them through the Kubernetes custom metrics API and, if you wish, the external metrics API. Many teams already run Prometheus for dashboards and alerts, so reusing it for autoscaling means no new data collection system is needed.
Prometheus is a giant library of records. The adapter is a librarian with a rulebook. When the HPA walks up and asks for “requests per second for these pods”, the librarian checks the rulebook to find which shelf holds the answer, fetches it, does a little arithmetic if needed, and hands over a clean answer in the format the HPA expects.
The rulebook: four ideas
The adapter is configured with a list of rules. Every rule answers four questions, and understanding them removes most of the mystery.
Which raw series does this rule cover?
A series query selects the Prometheus series the rule applies to, for example every series whose name starts with a certain prefix and that carries a namespace and pod label.
Which Kubernetes objects do the labels refer to?
The rule maps Prometheus labels such as “namespace” and “pod” to Kubernetes resources. This is what lets the API say “this number belongs to that pod”.
What should the metric be called in Kubernetes?
A raw counter ending in a total can be exposed under a friendlier name that ends in “per second”, so your HPA reads naturally.
How is the value calculated?
A query template fills in the right label matchers and grouping automatically. A typical template turns a growing counter into a rate over a short window.
Counters, rates and why they matter
Most application metrics are counters, numbers that only ever go up, such as “total requests served since this pod started”. A counter by itself is useless for autoscaling: a pod that has been up for a month has a huge number even if it is idle right now. What you need is the rate, meaning how fast the counter is growing per second. Prometheus can compute this over a recent window, for example the last two minutes. The adapter rule wraps the counter in that calculation, so the HPA sees “requests per second” rather than a meaningless total.
The window length is a trade-off. A short window reacts quickly but is noisy. A long window is smooth but slow to notice a real change. A window of around two to five times your scrape interval is a common starting point.
Checking that it works
A very helpful habit is to query the custom metrics API directly and confirm your metric appears for the expected pods before you create the HPA. If the metric is missing there, the HPA will never find it either. Most “my HPA shows unknown” problems are solved by this one check, because they come from a rule that selects nothing or a label that does not line up with Kubernetes object names.
| Symptom | Likely cause in the adapter |
|---|---|
| Metric does not appear in the API at all | The series query matches nothing, or the metric has no namespace or pod label |
| Metric appears for some pods but not others | Some pods are not scraped, or label names differ between workloads |
| Values look far too large or small | A counter was exposed without turning it into a rate, or the window is wrong |
| New metric not visible yet | The adapter has not yet refreshed its list; wait for the next relist interval |
Every HPA cycle triggers a Prometheus query for each metric. With dozens of HPAs, expensive queries add real load to Prometheus. Pre-compute heavy calculations as recording rules and let the adapter read the ready-made result.
KEDA: Event-Driven Autoscaling
KEDA takes a different path. Instead of translating one metrics store, it speaks directly to dozens of event sources.
The problem KEDA solves
Suppose your worker reads messages from Amazon SQS, Apache Kafka, RabbitMQ, or an Azure Service Bus queue. To scale it with the Prometheus Adapter you would first have to get those queue lengths into Prometheus through an exporter, then write adapter rules. KEDA removes those steps. It ships with a large library of scalers, each of which knows how to ask one kind of system “how much work is waiting?” Cloud queues, streaming platforms, databases, Prometheus itself, cron schedules and many more are covered.
If the Prometheus Adapter is a librarian for one library, KEDA is a receptionist with a phone directory. For each type of question it knows exactly which office to call, how to introduce itself, and how to summarize the answer in one number for the manager.
How KEDA is built
flowchart TB
SRC["Event Sources: queues, streams, databases, Prometheus, cron"] --> SCL["KEDA Scalers"]
SO["ScaledObject - your scaling rules"] --> OP["KEDA Operator"]
OP -->|"creates and manages"| HPA2["HPA"]
OP -->|"activates 0 to 1 and deactivates 1 to 0"| TGT["Target Deployment"]
SCL --> MS["KEDA Metrics Server - external.metrics.k8s.io"]
MS --> HPA2
HPA2 -->|"scales 1 to N"| TGT
TA["TriggerAuthentication / secrets / identity"] --> SCL
KEDA has a few parts. You describe your wishes in a custom object called a ScaledObject, which names the target workload, the minimum and maximum replicas, and one or more triggers (for example, “scale on the length of this queue”). The KEDA operator watches ScaledObjects and creates a regular HPA behind the scenes. A metrics server inside KEDA serves the numbers through the external metrics API, so the HPA reads them the usual way.
This is an important point for beginners: KEDA does not replace the HPA. It generates and feeds it. Everything you learned about the formula, tolerance and behavior settings still applies. KEDA’s additional skill is that it can also take a workload all the way down to zero replicas and bring it back, which the plain HPA does not do by default.
Scale to zero
Scaling to zero means running no pods at all while there is no work. This saves money for rarely used workloads, such as a nightly report builder or a development environment. The standard HPA can only do this with an alpha, off-by-default feature gate, so most teams do not rely on it. KEDA handles the 0-to-1 and 1-to-0 steps itself, and hands over to the HPA for 1-to-N scaling. Two settings control the rhythm: a polling interval (how often KEDA checks the source, 30 seconds by default) and a cooldown period (how long to wait after the last activity before going to zero, 300 seconds by default).
When a workload sits at zero, the first message must wait for a pod to be scheduled, pull its image, start up and pass readiness. That can take many seconds. Scale to zero suits background work, not user-facing requests with tight latency goals.
Jobs instead of long-running workers
For batch work, KEDA offers a ScaledJob. Instead of scaling a set of long-lived workers, it launches a separate Kubernetes Job for each batch of messages, and each Job finishes and disappears. This is useful when every task is long or heavy and you do not want one worker to be interrupted halfway by a scale-down.
Authentication
Event sources usually need credentials. KEDA keeps these out of your scaling rules by using a separate object, called a TriggerAuthentication, that points to Kubernetes secrets or to cloud identity systems. Using cloud workload identity is much safer than storing long-lived keys, because there is nothing secret to leak.
Where it is available
KEDA is open source and installs into any cluster. Several platforms also offer it as a managed feature. Azure Kubernetes Service has a KEDA add-on, and Red Hat’s OpenShift ships a Custom Metrics Autoscaler Operator that is based on KEDA.
| Option | Best for | Watch out for |
|---|---|---|
| Prometheus Adapter | Application metrics already in Prometheus, such as request rate per pod | Rules take effort to write; one provider per API group |
| KEDA | Queues, streams, schedules, scale to zero, many external sources | An extra component to run; avoid double control of the same workload |
| Cloud or vendor adapters | Teams already living in one monitoring product or cloud | Lock-in, and the cost of the metrics service |
| Zalando kube-metrics-adapter | Mixed sources like Prometheus and SQS, with simple annotations | Smaller community than the others |
Choosing the Right Metric
The tooling is the easy half. The hard half is deciding which number deserves to control your fleet.
A good scaling metric has three qualities. It must be causal: more load must raise it. It must be responsive: adding pods must lower it. And it must be timely: it should move before users feel pain, not after. Let us look at the common candidates through that lens.
| Metric | Good for | Strengths | Weaknesses |
|---|---|---|---|
| Requests per second per pod | Stateless web and API services | Directly tied to load; falls as pods are added | Requires you to know how many requests one pod can handle |
| Queue length per worker | Background job processors | Shows backlog before anything breaks | Slow tasks need a time-based view as well |
| Consumer lag | Kafka or stream consumers | Measures how far behind processing is | Adding pods beyond the partition count helps nothing |
| In-flight requests per pod | Services with long requests or WebSockets | Reflects concurrency accurately | Needs instrumentation in the app |
| Latency (p95 or p99) | A secondary guard | Close to user experience | Lagging, noisy, and often does not fall when pods are added |
| Error rate | Alerts, not scaling | Signals trouble | Adding pods may make a failing dependency fail harder |
Find the capacity number first
To pick a target such as “100 requests per second per pod”, you need to know what one pod can really do. The honest way is to run a load test. Increase traffic on a single pod until latency starts to climb, note the throughput just before that point, and then set your target at about 60 to 70 percent of it. That headroom absorbs the delay of scaling. Guessing here is the origin of many outages and many wasted dollars.
An elevator has a posted limit, say ten people. Nobody wants it to reach ten every time, because then the next person waits. A building manager adds a second elevator when the first one is regularly eight-tenths full, not when it is bursting. The target is a comfort level, not the absolute maximum.
Beware of metrics that do not respond
Consider latency caused by a slow database. Your pods are healthy, but every request waits on a shared database that is saturated. If you scale on latency, the HPA adds more pods. More pods send more queries, the database gets worse, latency rises, and the HPA adds even more pods. This is a runaway feedback loop, and it can turn a small problem into a full outage. The cure is to choose a signal that reflects your own capacity, and to set a sensible maximum replica count as a safety fence.
Combining signals
Because the HPA takes the largest result across metrics, you can combine a primary signal with a safety net. For example, scale mainly on requests per second, but also on CPU, so that a surprising change in per-request cost still triggers growth. Keep the list short, as every metric you add is another thing that can go missing and confuse the controller.
With Kafka, each partition is read by at most one consumer in a group. If you have 12 partitions, more than 12 consumers will sit idle. Set the maximum replicas to the partition count, or increase partitions first.
Tuning Speed and Stability
Reacting fast and staying calm pull in opposite directions. The behavior settings let you choose where to stand.
The flapping problem
Imagine the load wobbles around the target. The HPA adds pods, the load per pod drops below target, the HPA removes pods, the load per pod rises, and the cycle repeats. This constant yo-yo is called flapping. It wastes resources, restarts connections and can cause errors every time a pod terminates while handling requests.
A thermostat that switches the heater on at 20.0 degrees and off at 20.1 would click constantly. Real thermostats have a small gap, so they wait until the room is clearly cold before heating, and clearly warm before stopping. Stabilization windows give the HPA the same patience.
Stabilization windows
The stabilization window tells the HPA to look back at the recommendations from the last N seconds and pick the safest one. For scale-down, the default window is 300 seconds (five minutes): the HPA will only shrink to the highest replica count it recommended over the past five minutes. For scale-up, the default window is zero, meaning it reacts immediately. This asymmetry is deliberate: it is safer to add capacity quickly and release it slowly.
Scaling policies
Policies put a speed limit on each step. You can say “add at most 4 pods every 60 seconds” or “remove at most 10 percent of pods every 2 minutes”. When several policies apply, a select policy decides which wins: the one allowing the largest change, the smallest change, or none at all (which disables scaling in that direction). By default, scale-up may double the pods or add 4 pods every 15 seconds, whichever is more permissive, and scale-down may remove everything the formula suggests, once the stabilization window agrees.
Customer-facing API
Scale up fast, with a short or zero window and generous step size, since latency matters. Scale down slowly, with a window of five to fifteen minutes and small steps, so one calm minute does not remove capacity before the next wave.
Batch queue workers
Scale up moderately, since a few extra minutes of backlog is acceptable. Scale down more aggressively to save money, but make sure workers finish their current task before shutting down.
Predictable daily peaks
Add a scheduled floor with a cron trigger or a higher minimum during known busy hours, and let the metric-based scaling handle surprises above that floor.
Graceful shutdown
Scaling down is only safe if pods leave politely. When Kubernetes removes a pod, it sends a termination signal and waits a grace period before forcing it off. A well-behaved application stops accepting new work, finishes what is in progress, and exits. A readiness change plus a short pre-stop pause lets the load balancer stop sending traffic before the pod closes its doors. Without this, every scale-down creates a handful of failed requests, and users notice.
Make new pods ready quickly
The faster a new pod is ready, the less headroom you need. Small container images, warm caches loaded lazily, and a readiness probe that truly reflects readiness all shorten the delay. A startup probe helps slow-starting applications avoid being killed while they initialize.
Some teams keep a few low-priority placeholder pods that reserve space on nodes. When real pods need room, the placeholders are evicted instantly, and the node autoscaler replaces the room in the background. It trades a little money for a much faster burst response.
Security of the Autoscaling Pipeline
Autoscaling gives numbers the power to spend money and move servers. That power needs protecting.
Why security matters here
If an attacker or a bug can change a metric, they can change your infrastructure. A forged queue length could cause the cluster to scale up to its maximum, running up a cloud bill. A forged low value could cause the cluster to scale down during peak traffic, causing an outage. So the metric path deserves the same care as any other production dependency.
Imagine a factory where the manager hires or fires workers based on a number written on a whiteboard. Anyone who can erase and rewrite that number controls the payroll. You would want a lock on the whiteboard room and a record of who wrote what.
Protect the adapter connection
The API server talks to the adapter over TLS. Make sure the APIService is configured with a proper certificate authority bundle, and do not disable verification outside of throwaway tests.
Use least-privilege RBAC
Role-based access control decides who can create or edit HPAs and ScaledObjects. Limit those rights, since changing a scaling rule is changing production.
Control who can publish metrics
If any pod in any namespace can publish a metric that your HPA reads, one noisy tenant can steer another. Use namespaces, label rules and network policies to limit what feeds each autoscaler.
Avoid long-lived keys
Prefer cloud workload identity for reading queues and monitoring APIs. When secrets are needed, keep them in Kubernetes secrets or a secret manager and scope them to read-only access.
Always set a maximum
A sensible maximum replica count, plus namespace resource quotas, caps the damage from any mistake or attack.
Restrict who can reach Prometheus and the adapter
Network policies should allow only the API server to call the adapter and only the adapter to query the metrics store.
A runaway scaling loop is indistinguishable, on your invoice, from an attack. Alert on replica counts that sit at the maximum for a long time and on sudden jumps in cluster size.
Monitoring the Autoscaler Itself
You watch your application. You must also watch the thing that resizes it.
What the HPA tells you about itself
Every HPA records its own status. It shows the current and desired replica counts, the latest metric values it observed, and a list of conditions explaining its state. Three conditions are especially useful:
| Condition | Meaning | When it worries you |
|---|---|---|
| AbleToScale | The HPA can fetch and change the scale of its target | False means it cannot reach or modify the workload |
| ScalingActive | Metrics are available and the calculation works | False usually means the metric is missing or the adapter is unreachable |
| ScalingLimited | The desired count was capped by the minimum or maximum | True at the maximum for long periods means you need more headroom or a fix |
The HPA also emits events with clear reasons, such as failures to get a pods metric or an external metric. Describing the HPA with the standard Kubernetes command line tool shows these events, which makes it the first stop for any investigation.
Metrics about the autoscaler
The cluster add-on called kube-state-metrics turns the HPA’s status into Prometheus metrics, such as the maximum replicas, current replicas and desired replicas of each autoscaler. Graph those alongside your traffic and you can literally see the control loop at work: load goes up, desired replicas follows, current replicas catches up a minute later.
flowchart LR
APP2["Application Pods"] --> PM["Prometheus"]
KSM["kube-state-metrics"] --> PM
ADP2["Metrics Adapter"] --> PM
PM --> GR["Dashboards"]
PM --> AL["Alert Manager"]
AL --> ONC["On-call Engineer"]
Alerts worth having
- Pinned at maximum: an HPA has been at its maximum replica count for more than fifteen minutes.
- Metrics unavailable: the ScalingActive condition has been false for several minutes.
- Adapter health: the custom or external metrics API returns errors or high latency.
- Pending pods: pods remain Pending for longer than a few minutes, which means node capacity is the bottleneck.
- Flapping: the replica count changes direction many times in an hour.
Logging
Keep the logs of the adapter or KEDA components in your central logging system. When a metric disappears at 3 a.m., these logs show whether a query failed, a credential expired or a source timed out. Also log deployments and configuration changes to HPAs so you can connect a change in behavior to the change in rules that caused it.
Run a load test in a staging cluster that has the same autoscaling rules. Watch how long each stage takes and how the metrics move. A rule that looks perfect on paper often reveals a missing label or an unexpected delay on the first real test.
Design Patterns and Anti-patterns
Teams that run autoscaling for years end up using the same few patterns, and avoiding the same few traps.
Patterns that work
Backlog per worker
Scale queue consumers on messages waiting divided by workers. It is simple, it falls as you add workers, and it connects directly to how long a job waits.
Primary signal plus safety net
Use a business metric such as requests per second as the main driver and CPU as a backstop for surprises in per-request cost.
Scheduled floor, reactive ceiling
Raise the minimum replicas ahead of known peaks, like the start of a shopping sale, and let metrics handle everything above that floor.
Scale on concurrency
For long-lived requests, count in-flight work per pod. It stays accurate even when each request takes a very different amount of time.
Fast up, slow down
Use short windows and wide steps for growth, and long windows with small steps for shrinking. This is the safest default for user-facing services.
Layered capacity
Combine the HPA for pods, a node autoscaler for machines, and a small buffer of placeholder pods for rapid bursts.
Anti-patterns to avoid
The mistake
Scaling only on latency or error rate because “that is what users feel”.
Why it hurts
These signals often do not fall when you add pods, especially when a shared database or downstream service is the real bottleneck. The result is a runaway loop where more pods create more pressure.
Better approach
Scale on load or concurrency that your pods control. Use latency for alerting and as a secondary check, with a firm maximum replica count.
The mistake
No maximum replica count, or a maximum far above what dependencies can handle.
Why it hurts
A bug or traffic flood can scale you into a huge bill and crush your database or third-party services.
Better approach
Pick a maximum based on load tests and downstream limits, and alert whenever the HPA touches it.
The mistake
Letting two controllers manage the same workload, such as a hand-written HPA and a KEDA ScaledObject pointing at the same Deployment.
Why it hurts
The two fight over the replica count and neither behaves as designed.
Better approach
Choose a single owner for scaling each workload. If you adopt KEDA, let it create the HPA.
The mistake
Using a metric with huge label variety, such as one series per user or per request path, as an autoscaling source.
Why it hurts
High cardinality makes queries slow and expensive, and slow queries mean slow, unreliable scaling decisions.
Better approach
Aggregate to the level you need, usually per pod or per deployment, and pre-compute with recording rules.
The mistake
Setting a minimum of one replica for a critical service to save money.
Why it hurts
A single pod gives no availability during upgrades or node failures, and the first spike has no buffer.
Better approach
Keep at least two or three replicas, spread across zones, for anything users depend on.
Troubleshooting and Common Mistakes
When autoscaling misbehaves, the cause is almost always one of a short list. Work through it in order.
The classic symptom: unknown
The most frequent complaint is an HPA that shows unknown instead of a number next to its target. This simply means the HPA could not get a value. Use the list below to walk from the outside in.
Is the metric produced by the app?
Open the metrics page of one pod and confirm the number exists and changes with traffic. If it is not there, nothing downstream can fix it.
Is it in the metrics store?
Query Prometheus or your monitoring product directly. Check that scrape targets are healthy and labels such as namespace and pod are present.
Is the metrics API registered and healthy?
Check that the APIService for the custom or external metrics group reports as available. If it is not, the adapter is down, unreachable, or has a certificate or permission problem.
Does the API return the metric for the right objects?
Query the metrics API for the exact name, namespace and object. Mismatched names, or a rule that renames the metric, are common culprits.
Does the HPA reference the right things?
Confirm the target workload name, the metric name, the metric type, and the label selector, if any. A small typo is the most frequent reason of all.
Mistakes that look like bugs
| What you see | Likely reason | Fix |
|---|---|---|
| CPU based scaling shows unknown | The pods do not declare CPU requests, so a percentage cannot be calculated | Set resource requests on every container |
| HPA keeps adding pods but the metric stays high | The bottleneck is elsewhere, or the metric does not respond to capacity | Choose a different metric and cap the maximum |
| HPA does nothing though load is clearly higher | The ratio is inside the 10 percent tolerance, or the maximum has been reached | Lower the target or raise the maximum |
| Replicas bounce up and down | Short windows and a noisy metric | Smooth the query with a longer rate window and lengthen scale-down stabilization |
| New pods stay Pending | No node capacity or constraints cannot be satisfied | Check the node autoscaler, quotas and scheduling rules |
| Replica count resets after each deploy | A fixed replica count in the deployment definition overrides the HPA | Remove the fixed value when an HPA owns scaling |
| Scaling works but users still see errors | Pods receive traffic before they are truly ready, or shut down abruptly | Improve readiness probes and graceful shutdown |
Look at the HPA’s conditions and recent events first. They name the failure in plain words far more often than you would expect.
Real-World and Industry Examples
Here is how well-known organizations and platforms approach custom metrics scaling, plus a few realistic scenarios.
Zalando: an adapter built for mixed sources
The European fashion retailer Zalando open sourced an adapter called kube-metrics-adapter. It lets teams scale on a mix of sources such as Prometheus queries, Amazon SQS queue lengths and request rates reported by their ingress layer. It was built because the standard options of the time did not cover all of the signals their many teams needed.
Microsoft and Red Hat: KEDA
KEDA began as a joint project between Microsoft and Red Hat to serve event-driven workloads such as functions and queue consumers. It later joined the Cloud Native Computing Foundation and reached graduated status. Azure Kubernetes Service offers it as an add-on, and Red Hat’s OpenShift packages a KEDA-based operator, so many enterprises use it without installing it by hand.
Datadog: external metrics from a monitoring product
Teams that already use Datadog can have its Cluster Agent act as an external metrics provider. The HPA then scales on any number Datadog already tracks, including business measures, without a separate Prometheus pipeline.
Google Cloud and AWS: managed monitoring feeding the HPA
On Google Kubernetes Engine, an adapter exposes Cloud Monitoring metrics, such as the number of undelivered messages in a Pub/Sub subscription, to the HPA. On Amazon EKS, teams commonly pair Amazon Managed Service for Prometheus with the Prometheus Adapter for application metrics, and use KEDA for Amazon SQS queues.
Realistic scenarios
The following are illustrations of how the ideas combine. They are teaching examples and do not describe the internal systems of the companies they mention.
A ride-hailing app in the evening rush
Think of a service like Uber matching riders to drivers. The matching pods scale on in-flight match requests per pod, because that number rises with demand and falls when pods are added. Rainstorms and event endings create sharp spikes, so a scheduled floor is added for known busy hours and a short scale-up window handles the unplanned ones.
A video platform processing uploads
A streaming company like Netflix must convert each uploaded video into many formats. Encoding workers scale on the number of jobs waiting per worker, and because jobs are long, they use job-style scaling and graceful termination so no encode is interrupted by a scale-down. At night, with few uploads, they shrink toward a very small pool to save money.
A retailer’s flash sale
An online store such as Amazon or Zalando expects a surge at a published time. Rather than wait for the metric to react, the team raises the minimum replicas fifteen minutes before the sale and lets requests-per-second scaling take over once traffic arrives. They also pre-warm nodes and check that the database can handle the higher connection count.
A nightly report builder
A finance team runs a heavy reporting job once a night. With scale to zero, no pods exist in the daytime. When the night’s tasks appear in the queue, the system starts workers, processes the backlog, and returns to zero after the cooldown period, so they pay only for the hours the job actually runs.
Capacity Planning with Simple Math
You do not need advanced mathematics to size an autoscaled service. A few small formulas will carry you a long way.
Little’s Law in plain words
There is a beautiful rule from queueing theory called Little’s Law. It says that the average number of things inside a system equals the rate at which they arrive multiplied by the average time each one spends inside. In a web service, the “things” are requests that are currently being handled, the arrival rate is requests per second, and the time inside is the response time.
For example, if 2,400 requests arrive every second and each takes an average of 0.1 seconds to finish, then about 240 requests are being processed at any moment. This number is called concurrency. It is one of the most useful numbers in capacity planning, because it tells you how many requests your pods must juggle at the same time.
A coffee shop serves 2 customers per minute, and each customer spends 3 minutes in the shop from ordering to leaving. On average there are 6 people inside. If the shop has only 4 seats, people will queue at the door. Concurrency is the number of seats you need, and pods are your seats.
From concurrency to pod count
Once you know how many requests a single pod can handle at the same time while staying fast, the pod count is just a division. If one pod can comfortably hold 5 requests in flight, then 240 concurrent requests need 48 pods. Add a margin for safety and you have your target. This is the same answer you would get from the HPA formula, but now you understand where the number comes from instead of treating it as magic.
Three numbers every service owner should know
With those three numbers, the minimum and maximum replica counts almost choose themselves. The maximum is peak demand divided by the target per pod, plus a margin, but never more than your database and other dependencies can safely serve. The minimum is the larger of two things: the number of pods you need for availability (usually two or three across zones) and the number needed to handle the quietest time plus a sudden jump.
The reaction-time budget
Another useful calculation is how much extra load can arrive while you wait for new pods. If load can grow by 30 percent per minute and it takes two minutes to get new pods ready, you must carry about 60 percent spare capacity or accept slower service for a short time. If that is too expensive, work on reducing the delay or smoothing the load with queues, rather than keeping huge idle capacity all day.
Pods are placed on nodes according to the CPU and memory they request, not what they really use. If requests are far too high, nodes fill up on paper while sitting mostly idle, and your node autoscaler buys machines you do not need. Right-size requests from real measurements.
A Worked Design: An Order Processing Service
Let us put everything together in one realistic design, step by step, using plain numbers.
Imagine an online store with an order service. It receives order requests from the website and the mobile app, saves them, and puts a message on a queue for a separate fulfillment worker. We will design the autoscaling for both parts.
flowchart LR
CL["Web and Mobile Clients"] --> GW["API Gateway / Load Balancer"]
GW --> ORD["Order Service Pods"]
ORD --> DB["Orders Database"]
ORD --> Q["Message Queue"]
Q --> WRK["Fulfillment Worker Pods"]
WRK --> SHIP["Shipping and Payment Partners"]
PRM["Prometheus"] -->|"requests per pod"| ORD
PRM -.-> HPA3["HPA with Prometheus Adapter"]
KD["KEDA"] -->|"queue length per worker"| WRK
Measure one pod
A load test shows that one order service pod handles 80 requests per second before latency degrades. At that rate it holds 8 requests in flight (80 times 0.1 seconds).
Choose the target
The team sets the target at 50 requests per second per pod, which is about 62 percent of capacity. This leaves room for the two minutes it takes to add pods.
Set the limits
Night traffic is about 300 requests per second, which needs 6 pods. Weekday peak is 1,500, needing 30 pods. The biggest sale ever seen is 2,400, needing 48 pods. They choose a minimum of 4 for availability, and a maximum of 60, which is 25 percent above the sale peak and still within what the database can serve.
Tune the behavior
Scale-up has no waiting window and may add up to 50 percent more pods per minute. Scale-down waits ten minutes and removes at most 10 percent of pods every two minutes. Pods have a graceful shutdown of 30 seconds so in-flight orders finish.
Add a safety net
CPU is added as a second metric with a target of 70 percent. If a code change makes each request more expensive, CPU triggers growth even if the request rate looks normal.
Design the workers
Each fulfillment worker handles one message in about 2 seconds. The goal is that no message waits more than about 20 seconds. That means a backlog of at most 10 messages per worker, so the target is 10 messages per worker, and KEDA reads the queue length directly. Minimum is 2, maximum is 40, and cooldown is 5 minutes.
Plan for the sale
Fifteen minutes before a known sale, a scheduled trigger raises the API minimum to 30 pods. Real traffic then takes over from there. Alerts fire if either autoscaler reaches its maximum or if pods stay Pending.
Walking through a spike
At 12:00 a promotion goes live and traffic jumps from 300 to 1,200 requests per second in three minutes. The six running pods suddenly see 200 requests per second each, four times the target. The HPA computes 6 times 4, which is 24 pods, and begins adding them within the allowed step sizes. Over the next two minutes the pod count climbs through 9, 14 and 21 to 24. Meanwhile a queue of orders builds slowly behind the API. KEDA sees the queue grow past 10 per worker and brings the workers up. After the promotion ends, the system waits ten minutes, then gradually removes pods so that no sudden drop in capacity occurs.
The design started with measuring one pod, not with picking tools. Tools are easy to change. A wrong understanding of capacity is not.
High Availability and Reliability
Autoscaling should make your service more reliable, not less. A few extra settings make sure of that.
Spread pods across zones
A zone is a separate data center area inside a cloud region. If every pod lands in the same zone and that zone has trouble, your whole service goes down. Kubernetes lets you declare topology spread constraints, a rule that says “keep the pods roughly evenly distributed across zones”. When the HPA adds pods, the scheduler follows this rule, so the extra capacity also improves resilience.
Protect pods during maintenance
A PodDisruptionBudget tells Kubernetes the minimum number of pods that must stay available when machines are being drained for maintenance or removed by the node autoscaler. Without one, a scale-down of nodes could remove several pods of the same service at once. With one, Kubernetes removes them in a safe order. Make sure the budget is compatible with your minimum replicas, or draining nodes can get stuck.
A hospital must keep a minimum number of nurses on every floor, even while staff go on break. The rota is arranged so that people leave one at a time. A PodDisruptionBudget is that rota for pods.
Health checks that tell the truth
Kubernetes uses three kinds of probes. A liveness probe asks “is this pod stuck and in need of a restart?”. A readiness probe asks “can this pod serve traffic right now?”. A startup probe gives slow starters time to initialize. Autoscaling depends heavily on readiness: until a new pod is ready, it takes no traffic, so a readiness probe that passes too early makes new pods fail users, and one that passes too late wastes the scale-up.
Failure scenarios and recovery
| Failure | What happens | Protection |
|---|---|---|
| Metrics adapter crashes | The HPA cannot read values and holds the current replica count | Run adapter replicas, alert on API errors, keep sensible minimums |
| Metrics store is overloaded | Queries are slow or time out | Use recording rules, dedicated capacity, and short timeouts |
| A zone becomes unavailable | Pods there disappear and load rises on the rest, triggering scale-up | Spread across zones, keep headroom, ensure nodes can start in other zones |
| Cloud runs out of machines of your type | New pods stay Pending | Allow several instance types and keep a small capacity buffer |
| Downstream dependency is slow | Latency rises, and latency-based scaling would make it worse | Scale on load, cap the maximum, use timeouts and circuit breakers |
| Bad deploy increases cost per request | Per-pod load appears higher, HPA scales up heavily | Rollout checks, a maximum replica count, and cost alerts |
Autoscaling during a rollout
When you release a new version, a Deployment replaces old pods with new ones gradually. The HPA keeps working during this time, but metrics from new pods may look different for a while as caches warm up. Use readiness probes and a small surge allowance so that capacity does not dip during the rollout, and avoid pushing releases at the exact moment of your daily peak.
Cost Control and Efficiency
Saving money is one of the main reasons to autoscale. It is also an easy place to make false savings.
Where the money goes
You pay for machines, not for pods. A pod only costs money because it occupies space on a machine. So the real saving from scaling pods down comes when the node autoscaler can also remove an emptied machine. If pods are scattered thinly across many nodes, no node ever becomes empty, and the savings never reach your bill. Settings that help the scheduler pack pods tightly, and a node autoscaler that consolidates underused machines, turn pod-level savings into real money.
Think of a bus company. Reducing the number of passengers does not save money until a bus can be parked. If every bus keeps one passenger, you still run every bus. You must move the passengers together so that some buses can be switched off.
Practical cost levers
Honest resource requests
Measure real usage and set requests close to it. Over-requesting wastes capacity on every pod you scale.
Reserved baseline, flexible burst
Cover your steady minimum with discounted long-term capacity, and let autoscaled bursts run on regular or surplus capacity.
Cheaper but interruptible machines
Surplus cloud machines cost much less but can be taken back on short notice. They suit stateless workers that handle interruption well, mixed with a safe on-demand base for user-facing traffic.
Pay only when working
Occasional jobs and non-production environments can sleep entirely, so you pay nothing between bursts.
Budgets as guardrails
Maximum replicas and namespace quotas stop experiments or bugs from creating surprise bills.
Show teams their cost
When each team can see what its scaling choices cost, over-generous maximums and idle minimums get fixed quickly.
Advantages of custom metrics scaling
- Scaling reflects real user-facing load
- Works for queues, streams and event-driven jobs
- Can reduce idle cost significantly
- Rules are readable: “keep 50 requests per pod”
- Builds on standard Kubernetes APIs
Disadvantages and trade-offs
- More components to install, secure and monitor
- Delay at every hop, so it is never instant
- Wrong metric choices can cause runaway scaling
- Needs load testing to choose good targets
- Debugging crosses several systems
Advanced Topics and Testing Strategy
Once the basics are stable, these ideas help you handle bigger, trickier systems.
Predictive and scheduled scaling
The HPA reacts. It does not predict. For traffic with a clear daily or weekly rhythm, you can add a prediction by hand: use a cron-style trigger to raise the minimum ahead of the busy period. Some cloud providers offer predictive scaling for virtual machine groups that learns from history, but Kubernetes itself has no built-in predictive HPA, so in most clusters scheduled floors are the practical tool.
Autoscaling several tiers together
Real systems are chains of services. If the front end scales up but the service behind it does not, the bottleneck simply moves. Think about scaling as a pipeline: when the API doubles, the services it calls will need to grow too, and the database must be able to absorb it. Often the right answer is to scale the front end on request rate, scale the back-end on its own queue or concurrency, and protect the database with connection pooling and caching rather than hoping it will scale with them.
Multiple clusters
An HPA works inside one cluster. Companies that run many clusters, for example one per region, either let each cluster scale itself and balance traffic between them, or use multi-cluster tools. Some projects, such as Karmada, provide a federated version of the HPA for this purpose. For most teams, scaling each cluster independently behind a global load balancer is simpler and more robust.
Service level objectives
A service level objective (SLO) is a promise, such as “99 percent of requests finish within 300 milliseconds”. Autoscaling is one tool that helps you keep that promise, but SLO measures like latency are usually better used for alerts and capacity reviews than as the direct scaling input. The scaling metric should be a leading indicator, such as concurrency or backlog, that you have shown by testing to protect the SLO.
A testing strategy that works
Single-pod load test
Use a load tool such as k6, Locust or JMeter against one pod to find its real capacity and latency curve.
Ramp test in staging
Increase traffic gradually with autoscaling enabled. Record how long each stage takes, from the metric changing to pods being ready.
Spike test
Jump suddenly to peak traffic. See what users experience during the delay, and decide whether spare capacity or a queue is needed.
Scale-down and soak test
Drop traffic and watch for errors during pod removal. Run long enough to catch slow leaks and flapping.
Failure drills
Turn off the metrics adapter or block a zone on purpose, and confirm that alerts fire and the service stays healthy.
When tuning, adjust a single setting per experiment. Autoscaling has many interacting parts, and changing several at once makes it impossible to know which change helped.
Frequently Asked Questions
Short answers to questions that come up again and again.
No. Prometheus is popular, but any system that can serve the Kubernetes custom or external metrics API will do. Cloud monitoring products, Datadog and KEDA’s direct scalers all work without Prometheus.
Custom metrics describe something attached to a Kubernetes object, such as requests per pod. External metrics describe something outside the cluster, such as the length of a cloud queue. The API groups and the HPA metric types are different for each.
Yes. The HPA calculates a replica count for each metric and uses the largest, so the most demanding signal wins.
Not by default. It has long been an alpha feature behind a feature gate, so check your Kubernetes version before relying on it. KEDA supports scale to zero out of the box and is the usual choice.
Yes, as long as they do not serve the same API group. A common setup uses the Prometheus Adapter for the custom metrics API and KEDA for the external metrics API, with each workload controlled by only one of them.
Expect roughly one to two minutes from a change in load to ready pods if nodes are available, and longer if new machines must start. The scrape interval, HPA cycle, image size and readiness time all add up.
The letter m means milli, or one thousandth. So 500m is 0.5. It is just a unit, not an error.
Rarely. Many applications keep memory they have used, so it does not fall after load drops, and it does not predict load well. It is better as a protection than as a main signal.
Yes, as long as the workload exposes the scale interface. Be careful, though: stateful systems often need data to be rebalanced when replicas change, so test scaling behavior and use conservative scale-down rules.
Load test one pod to find its comfortable throughput, then set the target to about 60 to 70 percent of that. Review it after real traffic teaches you more.
The HPA cannot compute a new count and will keep the current replicas in most cases. Because of that, alert on metric availability and keep a reasonable minimum so a monitoring outage does not leave you under-provisioned.
For workloads that cannot be split across copies, or that waste resources because their requests are guessed wrong, the Vertical Pod Autoscaler helps. Use it for right-sizing and use the HPA for handling load changes, without both reacting to the same metric.
Summary and Key Takeaways
If you remember only a handful of things from this guide, make them these.
Custom metrics autoscaling is the practice of letting the numbers that describe real work, not just processor usage, decide how many copies of your application run. The machinery is a chain: your application publishes a number, a collector stores it, an adapter serves it through a Kubernetes API, the HPA applies a simple proportional formula, and the Deployment creates or removes pods. Each link adds some delay, and each link can fail, so a good design measures and protects every one.
Key Takeaways
- Choose causal, responsive metrics. Requests per pod, queue backlog per worker and in-flight concurrency usually beat CPU, memory or latency alone.
- Know the formula. Desired replicas equals current replicas times current value divided by target, rounded up, with a 10 percent tolerance and the largest answer winning across metrics.
- Pick the right provider. Use the Prometheus Adapter for application metrics already in Prometheus and KEDA for queues, streams, schedules and scale to zero, with only one provider per metrics API group.
- Scale up fast and down slowly. Use stabilization windows and policies to prevent flapping, and make shutdowns graceful so scaling never hurts users.
- Plan for the delay. Autoscaling is reactive, so keep sensible minimums, light images, quick readiness and optional spare capacity for sudden bursts.
- Protect and observe the pipeline. Secure the adapter path, set maximum replicas, and alert on unavailable metrics, pending pods and autoscalers pinned at their limits.
- Test with real load. Find each pod’s true capacity first, set targets with headroom, and confirm the whole chain works before production does it for you.