# HPA At Max Capacity

> An HPA sitting at maxReplicas has no headroom left. It wanted to scale further and could not. ZopNight flags it as a reliability finding rather than a cost one: the workload is being throttled by a ceiling somebody set once, often months before the traffic changed.

Source: https://zop.dev/integrations/kubernetes/recommendations/hpa-at-max-capacity
Updated: 2026-08-19

---

## A pinned HPA is a silent capacity limit

An HPA at maximum is not autoscaling any more. It has computed a desired replica count above
`maxReplicas` and been clamped. Every request beyond what the current pods can serve queues or
fails, and the autoscaler that was supposed to prevent that is already doing everything it is
permitted to do.

Nothing alerts on this by default. The HPA reports itself as working, because clamping is normal
behaviour rather than an error state.

## Why the ceiling is usually stale

`maxReplicas` is typically set once, at authoring time, as a guess at the upper bound plus
headroom. Traffic grows; the number does not. By the time the workload is regularly hitting it,
the person who chose it has often moved on.

The second common cause is a ceiling set deliberately as a cost guard, which is legitimate, and
means this finding is telling you the guard is now binding rather than that it is wrong.

## What to check before raising it

Raising `maxReplicas` only helps if the cluster can actually place more pods. Three things gate
that:

- **Node capacity.** Without cluster autoscaling or Karpenter, more replicas simply go Pending.
- **Resource quotas.** A namespace quota on CPU or memory caps total replicas regardless of the HPA.
- **Downstream limits.** More application pods against a fixed database connection pool moves the
  bottleneck rather than removing it, and can make things worse.

That last one matters most: scaling a stateless tier into a saturated datastore converts a slow
service into a failing one.

## Matching currentReplicas against maxReplicas

```bash
kubectl get hpa -A -o json | jq -r '
  .items[] | select(.status.currentReplicas == .spec.maxReplicas)
  | "\(.metadata.namespace)/\(.metadata.name)\tcur=\(.status.currentReplicas)\tmax=\(.spec.maxReplicas)"'
```

Sustained pinning is the signal. A brief touch during a traffic spike is the HPA working
correctly.

## The cost reading

Raising the ceiling increases spend, so this finding is not a saving. It is here because a
workload silently capped below its demand is a reliability problem that looks like a cost
control.
