Autoscaling
A PlatformApplication running a Deployment is autoscaled by default — you get a
HorizontalPodAutoscaler on CPU without writing any configuration at all. This
page covers what that default gives you, how to tune it, and how to scale on a
signal other than CPU.
What You Get by Default
Leave spec.autoscaling out of your manifest entirely and the operator still
creates all three of these:
| Resource | Configuration | What it does |
|---|---|---|
HorizontalPodAutoscaler | min 2, max 10, 80% CPU | Scales pod count on average CPU utilization |
PodDisruptionBudget | minAvailable: 1 | Keeps one pod serving through node drains and cluster upgrades — see Running a Fixed Number of Pods before dropping to a single replica |
VerticalPodAutoscaler | updateMode: "Off" | Records CPU and memory right-sizing recommendations without acting on them |
spec.autoscaling.enabled defaults to true. An application with no
autoscaling block receives the CPU autoscaler described above, which is why a
brand-new service starts at two pods rather than one. Two things change that: you
opt out explicitly, or your application mounts
ReadWriteOnce storage and must run as a
single pod.
The VerticalPodAutoscaler never resizes anything — it is created in
recommendation-only mode so you can compare your declared
spec.deployment.resources against real usage:
kubectl describe vpa YOUR_APP -n YOUR_NAMESPACE
Tuning the CPU Autoscaler
Set a floor and a ceiling on the replica count:
spec:
autoscaling:
minReplicas: 3
maxReplicas: 20
The replica range is the only part of the CPU autoscaler you control. The 80% CPU utilization target is fixed for every CPU-scaled application.
Utilization is measured against the container's CPU request, so the request you
declare is what decides when scaling starts. Leave
spec.deployment.resources.requests.cpu unset and you get the platform default of
50m, which puts the scale-out threshold at roughly 40m — low enough that a
moderately busy service adds pods sooner than you might expect. If your
application scales out earlier than it should, raise the CPU request before
touching the replica range.
The CRD accepts both fields and both are reserved for future use. Setting either
one changes nothing about how your application scales. To scale on memory, use a
memory trigger — there is a
worked example below, and it needs no
additional setup.
Running a Fixed Number of Pods
PlatformApplication has no replicas field — the pod count comes from the
autoscaler. To hold a steady count, set the floor and the ceiling to the same
value:
spec:
autoscaling:
minReplicas: 4
maxReplicas: 4
That works for any fixed count of two or more. For exactly one pod, turn
autoscaling off instead — a Deployment with no autoscaler and no replica count
falls back to the Kubernetes default of one:
spec:
autoscaling:
enabled: false # exactly one pod, and no PodDisruptionBudget
That combination keeps the PodDisruptionBudget at minAvailable: 1 while the
application runs exactly one pod, which leaves it with zero allowed
disruptions. Kubernetes then refuses to evict the pod at all:
Cannot evict pod as it would violate the pod's disruption budget.
Eviction is how a node is emptied, so the node your pod sits on can never be
drained — kubectl drain retries forever, and cluster scale-down and node
upgrades stall on it. Rolling deployments are unaffected, since those replace the
pod directly rather than evicting it, so the problem stays invisible until
someone tries to remove the node.
Setting enabled: false avoids it: no autoscaler means no
PodDisruptionBudget, and the single pod can be evicted and rescheduled
normally. At two or more replicas the budget always leaves at least one
disruption available, so minReplicas / maxReplicas is the right tool there.
Scaling on Something Other Than CPU
CPU is the wrong signal for plenty of workloads. A queue consumer can sit on a
million-message backlog without moving the CPU needle, and an internal tool may
only need capacity during office hours. Declaring spec.autoscaling.triggers
switches the application onto KEDA, which scales on the metric
you name.
Scale up for the working day and back down overnight:
spec:
autoscaling:
minReplicas: 1
maxReplicas: 5
triggers:
- type: cron
metadata:
timezone: "UTC"
start: "0 6 * * *"
end: "0 20 * * *"
desiredReplicas: "3"
Or scale on memory as well as CPU, which the CPU autoscaler on its own cannot do:
spec:
autoscaling:
minReplicas: 2
maxReplicas: 10
triggers:
- type: memory
metadata:
type: Utilization
value: "80"
- type: cpu
metadata:
type: Utilization
value: "80"
Declare both, as above, whenever memory is what you care about. Triggers replace
the CPU autoscaler rather than adding to it, so a lone memory trigger would
leave you with no CPU scaling at all. Neither of these two reads anything outside
the cluster, so both work with no further setup.
Each trigger is a type plus a flat map of metadata, both required — exactly the
shape KEDA's own scaler documentation uses, passed straight through to the
ScaledObject the operator creates for you. A trigger that has to reach a queue,
a topic, or a remote metrics backend needs a credential as well; see
Trigger Credentials before adopting one of those.
metadata is a string-to-string map, so numbers and booleans need quotes:
desiredReplicas: "3", not desiredReplicas: 3. An unquoted number fails schema
validation when the manifest is applied:
spec.autoscaling.triggers[0].metadata.desiredReplicas: Invalid value: "integer":
spec.autoscaling.triggers[0].metadata.desiredReplicas in body must be of type
string: "integer"
Two behaviors are worth knowing before you rely on triggers:
- Triggers replace the platform's CPU autoscaler. When
triggersis set the operator creates a KEDAScaledObjectinstead of aHorizontalPodAutoscaler, and removes the HPA it previously managed — so switching an existing application onto triggers needs no cleanup on your side, andenabled: truedoes not bring the CPU autoscaler back. KEDA then creates and owns its own HPA namedkeda-hpa-YOUR_APPto carry out the scaling. That one is expected: it is how KEDA applies its decisions, not a second autoscaler competing with yourScaledObject. minReplicasdefaults to0on the trigger path, where the CPU autoscaler defaults to2. An idle application scales all the way down to zero pods unless you set a floor — which is exactly what you want for a batch worker, and rarely what you want for anything serving traffic.
Trigger Credentials
KEDA reads your metric itself, from its own pod — not from inside your
application — so it needs its own path to the metric source. KEDA's mechanism for
that is a TriggerAuthentication object named by authenticationRef, and
spec.autoscaling.triggers models neither: a trigger is a type and a flat
metadata map, with nowhere to name a credential. The cloud identity attached to
your workload belongs to your pods, not to KEDA's, so it does not close the gap.
Contact Ybor support before committing to a trigger that reads a queue, a topic,
or a remote metrics backend. The credential is the part that needs arranging, and
it is arranged outside your PlatformApplication.
That splits the trigger types cleanly:
- Triggers that read nothing outside the cluster need no arranging.
cron,memoryandcpuare all self-contained, which is why the examples above use them. - Triggers that read a queue, a topic, or a remote metrics backend — SQS, Service Bus, Kafka, Redis, a remote Prometheus — need the credential conversation first. The YAML for these is the easy part.
The platform's own n8n installation is the worked example of the second kind: its
workers scale on Redis queue depth, and making that function took a
TriggerAuthentication delivered by n8n's Helm chart, bridging the Redis password
Secret into the scaler. That is the shape of the answer, and it lives outside your
PlatformApplication.
One practical detail if your metric source is inside the cluster: address it by
fully-qualified name, my-service.my-namespace.svc.cluster.local. KEDA resolves
the address from its own namespace, so a bare Service name will not find a Service
in yours.
Finding Your Cluster's Scaler Reference
KEDA ships as a platform component, so the exact version — and therefore the exact set of available scalers and their metadata fields — depends on your cluster. Check what you are running before reaching for a scaler:
kubectl get deployment keda-operator -n keda \
-o jsonpath='{.spec.template.spec.containers[0].image}'
Then look up your scaler in the KEDA scaler reference, selecting the version that matches. The cron scaler is the easiest one to start with: it needs no external metrics backend, so it is a good way to confirm the wiring works before you point a trigger at a real queue.
Autoscaling and Persistent Storage
A Deployment that mounts a ReadWriteOnce PersistentVolumeClaim can only ever
run one pod — a second replica would block forever waiting to attach the same
volume. The platform enforces this rather than letting you deploy something that
cannot scale: asking for horizontal scaling and an RWO volume together is rejected
when the manifest is applied, with a message spelling out your options.
If the application has to scale, it needs storage that isn't a single
ReadWriteOnce volume — either a volume type that supports ReadWriteMany, so
several replicas can mount it at once, or object storage the application reaches
over the network instead of through a mount. Which of those you can use depends on
the cloud your cluster runs in, so confirm what's available before designing
around one; Persistent Storage covers the
volume side, and Ybor support can tell you what your cluster offers.
Verifying What You Deployed
Check which autoscaler your application actually got:
kubectl get scaledobject -n YOUR_NAMESPACE
kubectl get hpa -n YOUR_NAMESPACE
What you should see:
- On the CPU path — no
ScaledObject, and oneHorizontalPodAutoscalernamed after your application. - On the trigger path — a
ScaledObjectnamed after your application, plus an HPA namedkeda-hpa-YOUR_APPthat KEDA created and owns. Read theScaledObjectrather than that HPA when you want to know whether your trigger was understood. - Neither — autoscaling is off, either because you set
enabled: falseor because an RWO volume pinned the application to a single pod.
If kubectl get scaledobject reports that the server has no resource of that type,
KEDA is not installed in your cluster and triggers cannot work there — contact
Ybor support.
When a ScaledObject exists but nothing scales, its events explain what KEDA made
of your trigger:
kubectl describe scaledobject YOUR_APP -n YOUR_NAMESPACE
Related
spec.autoscaling— full field reference- Non-Web Workloads — background services, scheduled jobs, and event-driven tasks
- Troubleshooting — when a deployment does not come up as expected