diff --git a/.changeset/bright-rabbits-attend.md b/.changeset/bright-rabbits-attend.md
new file mode 100644
index 00000000000..fc6e428fc28
--- /dev/null
+++ b/.changeset/bright-rabbits-attend.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/server-runtime": patch
+---
+
+Single Fetch: Do not try to encode a `turbo-stream` body into 304 responses
diff --git a/integration/helpers/create-fixture.ts b/integration/helpers/create-fixture.ts
index fe228466541..5f7966923bc 100644
--- a/integration/helpers/create-fixture.ts
+++ b/integration/helpers/create-fixture.ts
@@ -138,12 +138,13 @@ export async function createFixture(init: FixtureInit, mode?: ServerMode) {
     let url = new URL(href, "test://test");
     let request = new Request(url.toString(), init);
     let response = await handler(request);
-    let decoded = await decodeViaTurboStream(response.body!, global);
     return {
       status: response.status,
       statusText: response.statusText,
       headers: response.headers,
-      data: decoded.value,
+      data: response.body
+        ? (await decodeViaTurboStream(response.body!, global)).value
+        : null,
     };
   };
 
diff --git a/integration/single-fetch-test.ts b/integration/single-fetch-test.ts
index ec3de5e2ae8..870a97b94c6 100644
--- a/integration/single-fetch-test.ts
+++ b/integration/single-fetch-test.ts
@@ -1924,6 +1924,56 @@ test.describe("single-fetch", () => {
     ]);
   });
 
+  test("does not try to encode a turbo-stream body into 304 responses", async () => {
+    let fixture = await createFixture({
+      config: {
+        future: {
+          unstable_singleFetch: true,
+        },
+      },
+      files: {
+        ...files,
+        "app/routes/_index.tsx": js`
+          import { json } from "@remix-run/node";
+          import { useLoaderData } from "@remix-run/react";
+
+          const eTag = "1234";
+          export function loader({ request }) {
+            if (request.headers.get("If-None-Match") === eTag) {
+              throw new Response(null, { status: 304 });
+            }
+            return { message: "Hello from the loader!" };
+          };
+
+          export default function Index() {
+            const { message } = useLoaderData<typeof loader>();
+            return <h1>{message}</h1>
+          }
+        `,
+      },
+    });
+    let res = await fixture.requestSingleFetchData("/_root.data");
+    expect(res.data).toEqual({
+      root: {
+        data: {
+          message: "ROOT",
+        },
+      },
+      "routes/_index": {
+        data: {
+          message: "Hello from the loader!",
+        },
+      },
+    });
+    res = await fixture.requestSingleFetchData("/_root.data", {
+      headers: {
+        "If-None-Match": "1234",
+      },
+    });
+    expect(res.status).toBe(304);
+    expect(res.data).toBeNull();
+  });
+
   test.describe("revalidations/_routes param", () => {
     test("does not make a server call if no loaders need to run", async ({
       page,
diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts
index 9c404322d95..a28aff97369 100644
--- a/packages/remix-server-runtime/server.ts
+++ b/packages/remix-server-runtime/server.ts
@@ -413,6 +413,11 @@ async function handleSingleFetchRequest(
   let resultHeaders = new Headers(headers);
   resultHeaders.set("X-Remix-Response", "yes");
 
+  // 304 responses should not have a body
+  if (status === 304) {
+    return new Response(null, { status: 304, headers: resultHeaders });
+  }
+
   // We use a less-descriptive `text/x-script` here instead of something like
   // `text/x-turbo` to enable compression when deployed via Cloudflare.  See:
   //  - https://github.com/remix-run/remix/issues/9884