diff --git a/packages/astro/src/runtime/server/render/astro/render.ts b/packages/astro/src/runtime/server/render/astro/render.ts
index adc335495d..d608e5de08 100644
--- a/packages/astro/src/runtime/server/render/astro/render.ts
+++ b/packages/astro/src/runtime/server/render/astro/render.ts
@@ -125,7 +125,7 @@ export async function renderToReadableStream(
 					}
 
 					// Queue error on next microtask to flush the remaining chunks written synchronously
-					setTimeout(() => controller.error(e), 0);
+					queueMicrotask(() => controller.error(e));
 				}
 			})();
 		},
@@ -229,9 +229,12 @@ export async function renderToAsyncIterable(
 	// The `next` is an object `{ promise, resolve, reject }` that we use to wait
 	// for chunks to be pushed into the buffer.
 	let next: ReturnType<typeof promiseWithResolvers<void>> | null = null;
-	const buffer: Uint8Array[] = []; // []Uint8Array
+	const buffer: Uint8Array[] = [];
 	let renderingComplete = false;
 
+	const BATCH_SIZE = 1024 * 1;
+	let currentBatchSize = 0;
+
 	const iterator: AsyncIterator<Uint8Array> = {
 		async next() {
 			if (result.cancelled) return { done: true, value: undefined };
@@ -271,8 +274,9 @@ export async function renderToAsyncIterable(
 				offset += item.length;
 			}
 
-			// Empty the array. We do this so that we can reuse the same array.
+			// Empty the buffer and reset the batch size
 			buffer.length = 0;
+			currentBatchSize = 0; // **Reset batch size after sending**
 
 			const returnValue = {
 				// The iterator is done when rendering has finished
@@ -297,7 +301,9 @@ export async function renderToAsyncIterable(
 				renderedFirstPageChunk = true;
 				if (!result.partial && !DOCTYPE_EXP.test(String(chunk))) {
 					const doctype = result.compressHTML ? '<!DOCTYPE html>' : '<!DOCTYPE html>\n';
-					buffer.push(encoder.encode(doctype));
+					const doctypeBytes = encoder.encode(doctype);
+					buffer.push(doctypeBytes);
+					currentBatchSize += doctypeBytes.length; 
 				}
 			}
 			if (chunk instanceof Response) {
@@ -310,7 +316,13 @@ export async function renderToAsyncIterable(
 				// Push the chunks into the buffer and resolve the promise so that next()
 				// will run.
 				buffer.push(bytes);
-				next?.resolve();
+				currentBatchSize += bytes.length;
+
+				// Check if batch size threshold is reached
+				if (currentBatchSize >= BATCH_SIZE) {
+					next?.resolve();
+					// Note: Do not reset currentBatchSize here; it will be reset in `next()`
+				}
 			} else if (buffer.length > 0) {
 				next?.resolve();
 			}
diff --git a/packages/astro/src/runtime/server/render/page.ts b/packages/astro/src/runtime/server/render/page.ts
index 0e0bcf295a..94d27dfd88 100644
--- a/packages/astro/src/runtime/server/render/page.ts
+++ b/packages/astro/src/runtime/server/render/page.ts
@@ -7,6 +7,28 @@ import { renderToAsyncIterable, renderToReadableStream, renderToString } from '.
 import { encoder } from './common.js';
 import { isDeno, isNode } from './util.js';
 
+function readableStreamFromAsyncIterable(
+	asyncIterable: AsyncIterable<Uint8Array>,
+) {
+	const iterator = asyncIterable[Symbol.asyncIterator]();
+	return new ReadableStream(
+		{
+			async pull(controller) {
+				try {
+					const { value, done } = await iterator.next();
+					if (done) {
+						controller.close();
+					} else {
+						controller.enqueue(value);
+					}
+				} catch (err) {
+					controller.error(err);
+				}
+			}
+		},
+	);
+}
+
 export async function renderPage(
 	result: SSRResult,
 	componentFactory: AstroComponentFactory | NonAstroPageComponent,
@@ -59,9 +81,7 @@ export async function renderPage(
 				true,
 				route,
 			);
-			// Node.js allows passing in an AsyncIterable to the Response constructor.
-			// This is non-standard so using `any` here to preserve types everywhere else.
-			body = nodeBody as any;
+			body = nodeBody instanceof Response ? nodeBody : readableStreamFromAsyncIterable(nodeBody);
 		} else {
 			body = await renderToReadableStream(result, componentFactory, props, children, true, route);
 		}
diff --git a/packages/astro/src/runtime/server/render/util.ts b/packages/astro/src/runtime/server/render/util.ts
index 953488a839..63f53aa915 100644
--- a/packages/astro/src/runtime/server/render/util.ts
+++ b/packages/astro/src/runtime/server/render/util.ts
@@ -232,9 +232,22 @@ export type PromiseWithResolvers<T> = {
 	reject: (reason?: any) => void;
 };
 
-// This is an implementation of Promise.withResolvers(), which we can't yet rely on.
-// We can remove this once the native function is available in Node.js
+interface PromiseConstructorWithResolvers<T> extends PromiseConstructor {
+	withResolvers: () => PromiseWithResolvers<T>;
+}
+
+function hasWithResolvers<T>(
+	promiseConstructor: PromiseConstructor | PromiseConstructorWithResolvers<T>,
+): promiseConstructor is PromiseConstructorWithResolvers<T> {
+	return 'withResolvers' in promiseConstructor;
+}
+
+// This is an implementation of Promise.withResolvers(), which was added in Node v22
+// We can remove this once we drop support for Node <22.0.0
 export function promiseWithResolvers<T = any>(): PromiseWithResolvers<T> {
+	if (hasWithResolvers<T>(Promise)) {
+		return Promise.withResolvers();
+	}
 	let resolve: any, reject: any;
 	const promise = new Promise<T>((_resolve, _reject) => {
 		resolve = _resolve;