diff --git a/.changeset/flash-decode-per-request.md b/.changeset/flash-decode-per-request.md
new file mode 100644
index 00000000..c24fed50
--- /dev/null
+++ b/.changeset/flash-decode-per-request.md
@@ -0,0 +1,5 @@
+---
+"@solidjs/router": patch
+---
+
+The flash cookie is now detected, cleared and decoded once per request rather than once per router instance. A server render can re-create the router while it retries a suspension — a `useSubmissions` read under ``, inside a document shell, retries from above the router — and each new router restarted the decode, so the render never settled and the stream retried until the process ran out of memory. Each re-created router also appended another `Set-Cookie` clear.
diff --git a/src/routing.ts b/src/routing.ts
index 6919f90a..2ab8e11b 100644
--- a/src/routing.ts
+++ b/src/routing.ts
@@ -779,6 +779,23 @@ export function provideFlashDecoder(decoder: FlashDecoder): void {
flashDecoder || (flashDecoder = decoder);
}
+/**
+ * A request's flash cookie and its decode, shared by every router created
+ * for that request. A server render can re-create the router — a suspension
+ * under `` retries from the nearest hole above it, which may sit
+ * above the router — so state kept on one router instance would restart the
+ * decode with every re-creation and the seeding read would never settle.
+ * Keyed by the request event, so it lives exactly as long as the request.
+ */
+type FlashState = {
+ cookieHeader: string | null;
+ decode?:
+ | { done: true; value: FlashSubmission | undefined }
+ | { done: false; promise: Promise };
+};
+
+const flashStates = new WeakMap
}>{props.children}
+
+ );
+ }}
+
+
+ )).then(String),
+ delay(2000).then(() => "TIMEOUT")
+ ])
+ );
+
+ expect(html).toContain('class="status">Failed to save');
+ // the retry did render a new router; the request still decoded once and
+ // cleared the cookie once
+ expect(routerRenders).toBeGreaterThan(1);
+ expect(decodes).toBe(1);
+ expect(event.response.headers.getSetCookie().filter(c => c.startsWith("flash="))).toHaveLength(
+ 1
+ );
+ });
+});
diff --git a/vitest.config.server.ts b/vitest.config.server.ts
index cde6c4e8..979dde1a 100644
--- a/vitest.config.server.ts
+++ b/vitest.config.server.ts
@@ -2,12 +2,14 @@
// solid-js resolve to their server builds (isServer true, real request-event
// scoping via @solidjs/web/storage). Exercises the server integration in
// src/server.ts, which the jsdom suite cannot reach. The solid plugin runs
-// in ssr mode so specs can hand JSX trees to the collector.
+// in ssr mode so specs can hand JSX trees to the collector, and
+// compiles hydratable, as apps do: the hydratable server output is what
+// shapes where an async render retries from.
import { defineConfig, Plugin } from "vitest/config";
import solidPlugin from "@solidjs/vite-plugin";
export default defineConfig({
- plugins: [solidPlugin({ ssr: true, solid: { hydratable: false } }) as Plugin],
+ plugins: [solidPlugin({ ssr: true, solid: { hydratable: true } }) as Plugin],
resolve: {
conditions: ["node", "module", "development|production"]
},