Symptom

An architectural bug hunt flagged the Serializer polymorphic registry as an efficiency hotspot: “repeated per-request polymorphic resolution without caching or pre-warming,” blast radius every SSE frame and HTTP payload.

That hypothesis is refuted (see Root cause), but the investigation it prompted found a genuine defect one layer up, in the /sse serialize lambda — the single chokepoint every streamed node frame passes through. It was serializing each node twice per frame and discarding the first result. The discarded copy was the redacted one, so the frame that actually reached the wire carried unredacted SMTP tokens and auth-shaped outbound-webhook headers whenever demo mode was off — a silent revert of krill#917 for every normal deployment.

Root cause

Two independent parts.

The hypothesis, refuted. platformSerializerModule is a top-level val built exactly once at class-init, and kotlinx.serialization resolves polymorphic subtypes through a HashMap keyed by KClass — an O(1) lookup, not an O(n) walk over the 60+ registered subtypes. fastJson is likewise a singleton, and encodeToString<T> resolves the compile-time serializer, which the library caches per class on JVM. The two other files the lead named, UniversalAppNodeProcessor.kt and ServerProcessor.kt, contain no serialization calls at all. There is no per-call registry walk to cache.

The real defect. krill#917 (node-level secret redaction) and the demo-mode change were developed on branches that both edited the one-line /sse serialize lambda. The merge kept both edits stacked instead of composing them:

1
2
3
4
5
sse("/sse", serialize = { _, node ->
    fastJson.encodeToString<Node>((node as Node).redactSecrets())  // result discarded
    if (DemoModeContainer.enabled) { /* redacting encode */ }
    else { fastJson.encodeToString<Node>(node as Node) }           // NOT redacted
})

Kotlin’s block-bodied lambda returns its last expression, so the krill#917 line became a pure dead store: a full node copy plus a full JSON encode, computed and thrown away on every frame to every connected client. Demo mode masked half of it — its tree-walking pass blanks any field named token, so only non-demo deployments leaked. No test covered the lambda, because it was an anonymous inline block reading a global flag.

Fix

Extracted the lambda body into a pure, testable top-level function serializeSseNode(node, demoMode) in server/src/jvmMain/kotlin/krill/zone/server/SseNodeSerialization.kt, which redacts once into a local and feeds that single value to both branches. Routes.kt now calls it with DemoModeContainer.enabled. Result: one encode per frame instead of two (two instead of three in demo mode), and krill#917’s redaction holds on /sse in both modes. server/src/jvmTest/kotlin/krill/zone/server/SseNodeSerializationTest.kt covers both branches; the two non-demo cases fail against the pre-fix shape.

Separately, the same “serializer rebuilt per call” pattern turned up for real in composeApp/.../graph/GraphScreen.kt, which constructed a fresh Json { prettyPrint = true } inside a per-snapshot loop in a composable — hoisted to a file-level Json(from = fastJson) singleton.

Prevention