diff --git a/src/MeshWeaver.Hosting/DynamicTypePreWarmer.cs b/src/MeshWeaver.Hosting/DynamicTypePreWarmer.cs
index d31f863ff4..152adb37ae 100644
--- a/src/MeshWeaver.Hosting/DynamicTypePreWarmer.cs
+++ b/src/MeshWeaver.Hosting/DynamicTypePreWarmer.cs
@@ -55,9 +55,32 @@ public record PreWarmOutcome(string TypePath, PreWarmStatus Status, string? Deta
///
public static class DynamicTypePreWarmer
{
- /// How many dynamic NodeType hubs to activate concurrently. The compiles
- /// themselves serialize on the Compile IoPool; this just caps in-flight activations.
- public const int DefaultMaxConcurrency = 4;
+ ///
+ /// How many dynamic NodeType hubs to activate concurrently. ONE.
+ ///
+ /// 🚨 This was 4, and 4 is measurably harmful on a real mesh. On 2026-07-28 04:05 four
+ /// compiles were triggered on memex in quick succession — nothing to do with this warmer, but
+ /// the identical load shape — and within minutes SIX plugin roots (Claims, Edu, Underwriting,
+ /// Chess, Publish, Training) fell to the "did not settle" overlay and needed a scale-to-zero to
+ /// recover. That is the same storm this class's own summary warns about as the reason it ships
+ /// disabled, and the same one behind the 2026-07-22 "I have been told I am dead" restart
+ /// loop.
+ ///
+ /// The compiles already serialize on the Compile IoPool, so concurrency here buys nothing
+ /// except simultaneous cold ACTIVATIONS — which is exactly the expensive part: source discovery
+ /// on a large mesh is dominated by cold cross-partition hub activation (109ms and 0ms discovery
+ /// for the same NodeType shape on a fresh mesh, versus 45.20s on memex — issue #686). Warming
+ /// one at a time still removes the first-visitor stall; doing four at once trades a slow page
+ /// for an outage.
+ ///
+ public const int DefaultMaxConcurrency = 1;
+
+ ///
+ /// Pause between types, so a long warm sweep stays a background trickle rather than a queue of
+ /// back-to-back cold activations. Cheap insurance: the sweep is a latency optimisation with no
+ /// deadline, so there is no reason for it to ever look like load.
+ ///
+ public static readonly TimeSpan BetweenTypes = TimeSpan.FromSeconds(2);
/// Per-type warm budget — generous, because a cold Roslyn compile queued behind
/// others on the Compile IoPool can legitimately take a while. On elapse we log
@@ -120,7 +143,13 @@ public static IObservable WarmDynamicTypes(
return typePaths.Length == 0
? Observable.Empty()
: typePaths
- .Select(p => WarmOne(workspace, accessService, p, budget, logger))
+ // Space the types out as well as capping concurrency. With
+ // maxConcurrency=1 the Merge already serializes them, but back-to-back
+ // cold activations still read as a burst to the hubs being woken; the
+ // delay makes the sweep a trickle. It costs nothing — the warm-up has
+ // no deadline and Part 2 compiles lazily regardless.
+ .Select((p, i) => WarmOne(workspace, accessService, p, budget, logger)
+ .DelaySubscription(TimeSpan.FromTicks(BetweenTypes.Ticks * i)))
.Merge(Math.Max(1, maxConcurrency));
})
.Catch(ex =>
diff --git a/test/MeshWeaver.Hosting.Monolith.Test/DynamicTypePreWarmerTest.cs b/test/MeshWeaver.Hosting.Monolith.Test/DynamicTypePreWarmerTest.cs
index 47b8474fcf..db8a656475 100644
--- a/test/MeshWeaver.Hosting.Monolith.Test/DynamicTypePreWarmerTest.cs
+++ b/test/MeshWeaver.Hosting.Monolith.Test/DynamicTypePreWarmerTest.cs
@@ -81,4 +81,31 @@ await NodeFactory.CreateNode(new MeshNode("BrokenType", Partition)
broken.Status.Should().Be(PreWarmStatus.CompileError,
"a non-compiling type surfaces a bounded CompileError — never a hang, never blocking the good type");
}
+
+ ///
+ /// 🚨 THE STORM GUARD. The warm-up must default to ONE activation at a time.
+ ///
+ /// It shipped defaulting to 4, and 4 is measurably harmful: on 2026-07-28 04:05 four
+ /// compiles fired at memex in quick succession — the identical load shape — and within minutes
+ /// SIX plugin roots fell to the "did not settle" overlay and needed a scale-to-zero. That is the
+ /// storm this class's own summary cites as the reason it ships disabled, and the same one behind
+ /// the 2026-07-22 "told I am dead" restart loop.
+ ///
+ /// Concurrency buys nothing here — the compiles already serialize on the Compile IoPool —
+ /// so the only thing it adds is simultaneous cold ACTIVATIONS, which is the expensive part
+ /// (issue #686: the same NodeType shape is 0ms discovery on a fresh mesh and 45.20s on memex).
+ /// Pinned as a value because the damage only shows up at production scale, where no test runs.
+ ///
+ [Fact]
+ public void DefaultConcurrency_IsOne_AndTypesArePaced()
+ {
+ DynamicTypePreWarmer.DefaultMaxConcurrency.Should().Be(1,
+ "warming several cold NodeType hubs at once is what knocks roots offline; the sweep has "
+ + "no deadline, so it must trickle");
+
+ DynamicTypePreWarmer.BetweenTypes.Should().BeGreaterThan(TimeSpan.Zero,
+ "back-to-back cold activations still read as a burst even at concurrency 1");
+ DynamicTypePreWarmer.BetweenTypes.Should().BeLessThan(TimeSpan.FromSeconds(30),
+ "…but the sweep must still finish in a sensible time on a mesh with many types");
+ }
}