Platform defaults
Cluster-wide defaults for resources, timeouts, concurrency and the database connection budget, set once by the cluster admin and inherited by every app that leaves the field unset.
A KnextPlatform is a single, cluster-wide configuration object. It lets the person who runs the
cluster set the defaults that every app inherits, without editing each app. An app that sets a
field itself always keeps its own value.
Absent means unchanged. Until you create a KnextPlatform, nothing about your apps differs
from a cluster that has never heard of it. The install bundle ships the custom resource
definition but no KnextPlatform object, so upgrading the operator changes no app. An
empty KnextPlatform (spec: {}) and profile: default change nothing either.
What it sets today
The platform can supply a default for these fields. Every field is optional, and a field you leave out keeps its built-in value.
| Area | Field | Built-in value | Applies to an app that leaves this unset |
|---|---|---|---|
| Resources | resources.defaults.cpuRequest | 250m | spec.resources.cpuRequest |
resources.defaults.cpuLimit | 1000m | spec.resources.cpuLimit | |
resources.defaults.memoryRequest | 512Mi | spec.resources.memoryRequest | |
resources.defaults.memoryLimit | 1Gi | spec.resources.memoryLimit | |
| Requests | limits.timeoutSeconds | 300 | spec.timeoutSeconds |
| Scaling | scaling.defaults.containerConcurrency | 20 | spec.scaling.containerConcurrency |
scaling.defaults.scaleDownDelay | Knative's own default | spec.scaling.scaleDownDelay | |
scaling.defaults.targetBurstCapacity | Knative's own default | spec.scaling.targetBurstCapacity | |
scaling.defaults.panicWindowPercentage | Knative's own default | spec.scaling.panicWindowPercentage | |
scaling.defaults.panicThresholdPercentage | Knative's own default | spec.scaling.panicThresholdPercentage | |
| Database | database.connectionBudget | 80 | the cap that maxScale × poolMax must fit within |
| Rollout | rollout.maxAppsPerMinute | 10 | how fast a platform change reaches your apps |
It does not choose your runtime (Node or Bun stays a per-app choice), change any Knative configuration, or create a database, cache or ingress.
Create one
The object is cluster-scoped and must be named default. Any other name is rejected.
apiVersion: platform.kn-next.dev/v1alpha1
kind: KnextPlatform
metadata:
name: default
spec:
resources:
defaults:
cpuLimit: "2"
memoryLimit: 2Gi
limits:
timeoutSeconds: 600kubectl apply -f platform.yamlApply it with kubectl or through GitOps. The knext CLI never writes it.
Who wins
For every field, per app:
- the value on the app, if it set one;
- otherwise the platform's value, if it set one;
- otherwise the built-in value.
The merge happens when the operator renders the app. Your NextApp is never rewritten, so GitOps
diffs stay clean, and a platform edit reaches every app without touching any of them. Resources
merge field by field: an app that sets cpuLimit still inherits the platform's memoryLimit.
A preview deployment never keeps a scale-down delay, so it ignores scaling.defaults.scaleDownDelay
just as it ignores its own.
One caveat for resources. If an app's knext.config.ts sets any one of scaling.cpuRequest,
scaling.cpuLimit, scaling.memoryRequest or scaling.memoryLimit, the CLI fills in the other
three with its own defaults, and those count as set by the app. To let the platform supply the
rest, set none of the four in knext.config.ts.
See what an app inherited
kubectl get nextapp my-app -o jsonpath='{.status.platform}'
kubectl get nextapp my-app -o jsonpath='{.status.conditions[?(@.type=="PlatformDefaultsApplied")]}'status.platform records the platform generation and profile the app was last rendered against
and the exact fields it inherited, for example spec.resources.memoryLimit. It is absent while no
platform is in force. The PlatformDefaultsApplied condition says what happened:
| Reason | Meaning |
|---|---|
Inherited | The platform supplied at least one value this app left unset. |
NothingToInherit | A platform is in force but supplies nothing this app leaves unset. |
NoPlatform | There is no KnextPlatform named default. Built-in values apply. |
NoPlatformCRD | The KnextPlatform definition is not installed. Built-in values apply. |
PlatformNotAccepted | The object failed the operator's own validation and is ignored. |
EffectiveSpecInvalid | The platform's value would make this app invalid. The app's current service is held (see below). |
RolloutPending | A platform change is queued for this app (see below). |
The platform reports on itself too: Accepted, DefaultsPropagated and Ready, plus how many
apps are applied, pending and held.
kubectl get knextplatform default -o yamlWhat happens when you change it
Only apps whose values actually change get a new revision. Each changed service is a new revision, and a new revision starts a pod. An edit that changes nothing for an app, because it sets that field itself, creates no new revision for it.
Changes are paced. The operator re-renders affected apps at rollout.maxAppsPerMinute
(default 10), one every 60 ÷ N seconds, so one edit cannot start a pod for every app at once. A
queued app shows RolloutPending until its turn. Your own deploy is never queued behind this: if
you change the app itself, it rolls immediately with the platform's values included.
A value that would break an app is held, not applied. If a platform change makes an app's
combined settings invalid, for example a lowered connection budget that its maxScale × poolMax
no longer fits, or a CPU request above the limit, the operator leaves that app's running service
exactly as it is, reports EffectiveSpecInvalid naming the field, and raises a warning event.
Fix the app or the platform and the next reconcile applies it. An app that is invalid on its own
is rejected as it always was.
If the change being held is one you made to the app, for example a new image, the old version
keeps serving, so the app reports Ready: False with the same reason, and
kubectl wait --for=condition=Ready does not succeed until the change is applied. A platform
change on its own, with the app untouched, leaves Ready as the running service reports it.
Deleting the platform reverts every app to the built-in values, at the built-in pace of 10 apps per minute.
The connection budget
database.connectionBudget replaces the built-in 80 that maxScale × poolMax must fit within
for an app that declares a poolMax. The check at kubectl apply time and the check inside the
operator use the same number. It creates no database, pooler or connection of its own.
Lowering the budget does not roll out changes to an app that is already over it. There is no
grace for an app that was valid before: once its maxScale × poolMax is above the new budget,
the operator holds every change to that app and keeps its running service as it is. The change
to the app itself is still accepted at kubectl apply time, so deleting the app, editing its
labels or annotations, and a deploy that only changes the image all go through, but a new image
does not start serving until the app fits. An update that raises maxScale × poolMax above the
budget is rejected, and the rejection names the budget.
To get unstuck, do either of these:
- raise
database.connectionBudgetback to a value the app fits within; or - lower the app's
maxScaleorpoolMaxinknext.config.tsuntil the product fits, then deploy. That change is accepted because it fits.
Before lowering the budget, list what would be over it:
kubectl get nextapp -A -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,MAXSCALE:.spec.scaling.maxScale,POOLMAX:.spec.scaling.poolMax'A blank column means that field is unset on the app.
Observability
A held app keeps serving its old version, so nothing about it looks broken from the outside. The operator exports fleet-level metrics for the platform layer on its existing metrics endpoint, so you can alert on it without listing every app.
| Metric | Type | What it tells you |
|---|---|---|
knext_platform_apps_held | gauge, label reason | How many apps are held right now. reason is EffectiveSpecInvalid or RolloutPending. |
knext_platform_holds_total | counter, label reason | How many times an app entered a hold. A hold that lasts many reconciles counts once. |
knext_platform_rollout_pending | gauge | Re-renders waiting for their turn behind rollout.maxAppsPerMinute. |
knext_platform_rollout_wait_seconds | histogram | How long a queued re-render was told to wait for its turn. Re-renders that go straight through are not observed. |
knext_platform_budget_fallback_total | counter, label cause | Admissions that ignored the platform's connectionBudget and used the built-in 80. cause is read_error (the platform could not be read) or platform_invalid (it exists but is not valid). |
The fallback counter only counts the abnormal paths. A cluster with no KnextPlatform, or one
that sets no connectionBudget, uses the built-in value by design and is not counted, so any
increase is worth a look: the budget you set is not the one being enforced at kubectl apply.
Two alerts that cover the quiet failures:
# An app has been held for a while: a platform change is not reaching it.
- alert: KnextPlatformAppsHeld
expr: max(knext_platform_apps_held{reason="EffectiveSpecInvalid"}) > 0
for: 15m
# The admission check stopped honouring the platform's budget.
- alert: KnextBudgetWebhookFallback
expr: increase(knext_platform_budget_fallback_total[15m]) > 0The gauges and the queue reflect the operator process that is running now. After an operator
restart they refill as each app is reconciled again, and a queue that was in progress is re-spaced
from the start, still within maxAppsPerMinute.
Profiles
spec.profile accepts default or fastColdStart.
defaultsets nothing. It is the built-in values.fastColdStartis accepted but sets no value in this release, and no claim is made for it.
Upgrade order
Operator and definition first, then the KnextPlatform, then the CLI. A KnextPlatform applied
before its definition exists is rejected. An operator older than this feature ignores the object,
and apps return to the built-in values.
If the definition is missing, the operator still starts and behaves as it did before. Install the definition and restart the operator to switch the platform layer on.