The NodeType bake gate (PreWarm:GateReadiness, the nodetype_bake check, read by /ready) refuses readiness when a type that used to build no longer builds on this image. Its message states the contract: "refusing readiness so the rollout stalls with the previous image still serving". The gate is only safe while that clause is true. On 2026-09-25/26 it was false, and the gate took the whole control instance down (#5544).

What happened

After the V58 migration on memex.systemorph.com:

BinaryClickerV2/BinaryToggle is a demo type whose layout source calls a LayoutAreaHost.GetData<T>() that does not exist (CS1929, #3883). It has failed on every image since it was written. It is not a regression of any image.

Why a type that never built read as "regressed"

Two defects, and each one hid the other.

  1. "Not known to be broken" was read as "working". NodeTypeBakeEntry.WasHealthy is true for every state except PreviouslyBroken, including NeverBuilt. That is correct for what it says, because a type nobody has built is not damaged goods. But the regression baseline was built from it. A never-built type has nothing to regress from, so it was filed as a regression anyway. The first-bake rule (#4472) covered only the case where every type in the report is NeverBuilt. One never-built type on an established instance was still treated as a regression.
  2. The record could never learn the type was broken. A failed compile writes an Error stamp, and that stamp is a MeshPublicationGate publication. The gate holds it while the sweep runs and discards it when the pod is refused. So the failure refused the pod, the refused pod could not record the failure, and the next pod read the record as Ok again. That loop is why the record said Ok while the compile failed on every boot (#3883), and why #1391 was closed on a Not found and then came back.

The two rules

The per-type question and the report-level question are both answered in one stamp, DynamicTypePreWarmer.BaselineStamp. The sweep and BuildProtocolDriver.OutcomesOf both use it, so the GO and the gate cannot disagree (#4496). The answer is carried to the gate as PreWarmOutcome.HasRegressionBaseline. WasHealthyBeforeBake keeps its meaning.

Rule 1: a regression needs a working build to regress FROM, built by another, older image. NodeTypeBakeEntry.IsRegressionBaselineFor(livePlatformVersion) is true only when:

If this same build produced the working build, the image can build the type, so a failure now comes from content or the environment. If a NEWER build produced it, this process is the OLD image of a roll, and refusing it only removes the replicas the rollout falls back on. A record with no producer stamp keeps the strict reading.

Rule 2: an image that has already served this mesh never refuses itself. NodeTypeBakeReport.ThisBuildHasServed is true when either of two witnesses says so. Both are admission-gated publications, so a refused pod can write neither:

The marker is the witness that matters in the ordinary case. The first cut of this fix relied on record provenance alone, and review showed why that is not enough. An ordinary roll compiles nothing, because the compatibility key is equal across builds of one epoch. Prebuilt adoption keeps the PRODUCER's platform version on purpose. So a serving image can leave no record naming itself, and a restart of it would read as a stranger. A pod that finds either witness is a restart of a serving image, not a roll candidate, so nothing it reports may gate. NodeTypeBakeReport.GateRelevant, which the witness-unreadable path refuses on, applies the same two rules.

Together: a pod can still refuse on a regression only if both hold:

Once a build has been admitted with this code, its own pods never meet the second condition, so the old ReplicaSet can always come back. Before that, rule 1 still holds: a type is never a regression of an image that is not newer than the image that built it. Failures that no longer gate are still recorded and named in the health payload (WithoutBaseline). Once the pod is admitted, the held Error stamp is released, and the record finally says Error. That ends the loop in defect 2.

What the refusal looked like from outside: a death every 3.03 hours

The refusal had one more cost, and it was mistaken for a separate fault. Between 2026-09-25 ~18:00Z and the break-glass at 04:05Z, every memex portal container on both images died about 3.03 h after it booted. There was no crash dump for these deaths. Silo departures of this kind produce the Orleans timeout family filed as #5704 (folding #5709, #5730, #5705). That issue was opened from earlier, 2026-09-24 samples, so most of its evidence predates this window and is only related. The in-window evidence is narrower: Dequeue timeouts at 2026-09-25 18:57:39–41Z against silo S10.244.3.247, the incarnation of 56fbdcd48f-cbms9, after that pod's 18:55:12Z SIGABRT. The in-window samples tie these timeouts to a crash, not to a probe kill. That the probe kills produce the same lines is inferred, not observed.

Nothing deleted those pods. The kubelet killed each container when its startup probe ran out of budget. The readings, all taken 2026-09-26:

reading value
memex startupProbe (from the record, deployments/aks/memex/values.memex.public.yaml) /health, periodSeconds: 10, failureThreshold: 1080, so 10 800 s = 3 h
boots of 7bd794f9b5-ggf88 ([PlatformStartup]) 17:58 → 20:59 → 00:01 → 03:03, a 3 h 01–02 m period
its [LIVENESS] heartbeat (10 s) reaches tick ~1085 and never 1088+, so death comes at about 10 850 s
its log, once per 10 s up to its last minute (e.g. 05:51:57Z) Health check nodetype_bake with status Unhealthy … 1 NodeType(s) regressed on this image: BinaryClickerV2/BinaryToggle
kubelet event 05:52:27Z Startup probe failed: HTTP probe failed with statuscode: 503 on the same pod, 3 h into its fourth boot
56fbdcd48f-cbms9 (ci.9218) had served for ~29 h, SIGABRTed at 18:55Z (#4654), came back 18:58Z, then died at 22:00 and 01:02

So the loop was:

  1. A container starts on any image.
  2. /health stays red because the gate refuses.
  3. After 1080 failed probes the kubelet kills the container and restarts it in place, with the same pod name and restartCount + 1.
  4. The next attempt reaches the same verdict.

The previous image did not stay safe either. A serving pod that crashed for an unrelated reason (cbms9 above) had to pass the startup probe again, and could not. The replicas that the stall clause counts on were exactly the ones the gate turned away.

The pods that looked "deleted and recreated" after 04:05Z are a different event. The break-glass kubectl set env created a new ReplicaSet, and the ordinary rollout then scaled the old ones down (SuccessfulDelete … 84f4fb6dcc-wnhdr at 05:53:47Z). Its second pod had waited Pending from 04:07Z to 05:52Z on Insufficient cpu (silos requests at 55–91 %, autoscaler at max node group size reached), which is why that roll took 1 h 47 m.

How to recognise it:

Sample now records lastTerminationReason, lastExitCode and containerStartedAt per replica (Systemorph/MeshWeaver.Plugins#2397). That separates this kill from a crash (134/139) or an OOM kill without kubectl. Before that change a restart count was all it carried.

Not established: why these kills left no Application is shutting down line in Loki when the two rollout deletions did. The /drain endpoint's Drain: lines, which would record whether preStop and SIGTERM ran, returned zero lines in Loki over the 14 h, including for the rollout deletions. So these images do not emit them, and their absence proves nothing either way.

The gate holds readiness only

Policy bake-gate-readiness-only. The startup probe proves only that the process booted. The gate's verdict is read by readiness (/ready) alone. A refusal therefore stalls a roll: the new pod stays alive, it is not in the Service, the previous image keeps serving, and nothing is killed. A restarted pod of the previous image keeps serving too, because the startup probe it must pass no longer carries the verdict.

The two rules above made the gate unable to refuse on the serving image. They could not stop the second failure: the verdict rode the startup probe. On memex the startup probe read /health with periodSeconds 10 × failureThreshold 1080, a three-hour budget. A startup probe that never records a success kills the container at the end of that budget and restarts it into the same verdict. So from 2026-09-25 ~18:00Z every portal container of both images died 3.03 h after it booted, and a restarted pod of the serving image had to pass the same probe. The control instance was down from 20:54Z to 04:07Z (#5704).

How it is wired

Which checks may fail the startup probe

The question for each check is whether killing and restarting the container is the right answer to its "no". If the "no" is a property of the image, or of the roll, a restart cannot change it, and the check belongs on readiness.

Check Registered in Can fail the startup probe? Why
nodetype_bake Plugins host, if (gateBake) No: roll gate, /ready only Its verdict is about the image. A restart reaches the same verdict, and killing a previous-image pod removes the roll's fallback.
db_version (+ DbVersionGate) Plugins host Yes A portal ahead of its schema must not serve. A restart is the right retry once the migration Job has run. DbVersionGate stops the process itself at startup.
required_modules Plugins host No: roll gate, /ready only (policy required-modules-readiness-only) A declared-required module that the image should ship is absent from the pod's shelf, or a present one did not install against this platform (both Unhealthy). A store-delivered module not here yet is Degraded, a 200 that holds nothing, as before. If the registry cannot serve a module, it is missing for every image, so on the startup probe it would kill the previous image's restarted pods too, the same failure as the bake gate. As a roll gate a missing module stalls the roll and keeps the pod out of the Service. The cost: a restart no longer re-runs the bundle-fetch init container by itself. A pod whose modules arrive later needs a governed Restart.
process_progress (live) Plugins host Yes, and it restarts via /alive A GC-bound process is fixed by a restart.
PostgreSql Plugins host Yes No database, no portal.
self (live, ready) core Never fails The process can run a delegate.
content-types, storage_capacity, data_volume_free_space, pending_module_activation, bundle_adoption, entitlement_anchor, view_packs core / Plugins host No: Degraded at worst, which is a 200 Readings, never verdicts.
bake-report, source-discovery, publication-seal (census) core No: Degraded at worst, which is a 200 They always print; they never gate.

This table is correct for core's main and Plugins' main on the day it was written. A check added later that can answer Unhealthy must be placed deliberately: startup (/health, the container is killed) or roll gate (/ready, the roll stalls).

What this does not cover

Mesh Admission · NodeType Compilation · Deployment (AKS)