diff --git a/examples/suspense/.gitignore b/examples/suspense/.gitignore
new file mode 100644
index 000000000..6240da8b1
--- /dev/null
+++ b/examples/suspense/.gitignore
@@ -0,0 +1,21 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
diff --git a/examples/suspense/.vscode/extensions.json b/examples/suspense/.vscode/extensions.json
new file mode 100644
index 000000000..22a15055d
--- /dev/null
+++ b/examples/suspense/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+  "recommendations": ["astro-build.astro-vscode"],
+  "unwantedRecommendations": []
+}
diff --git a/examples/suspense/.vscode/launch.json b/examples/suspense/.vscode/launch.json
new file mode 100644
index 000000000..d64220976
--- /dev/null
+++ b/examples/suspense/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "command": "./node_modules/.bin/astro dev",
+      "name": "Development server",
+      "request": "launch",
+      "type": "node-terminal"
+    }
+  ]
+}
diff --git a/examples/suspense/README.md b/examples/suspense/README.md
new file mode 100644
index 000000000..e81359a7d
--- /dev/null
+++ b/examples/suspense/README.md
@@ -0,0 +1,47 @@
+# Astro Starter Kit: Minimal
+
+```
+npm create astro@latest -- --template minimal
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json)
+
+> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
+
+## 🚀 Project Structure
+
+Inside of your Astro project, you'll see the following folders and files:
+
+```
+/
+├── public/
+├── src/
+│   └── pages/
+│       └── index.astro
+└── package.json
+```
+
+Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
+
+There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
+
+Any static assets, like images, can be placed in the `public/` directory.
+
+## 🧞 Commands
+
+All commands are run from the root of the project, from a terminal:
+
+| Command                | Action                                           |
+| :--------------------- | :----------------------------------------------- |
+| `npm install`          | Installs dependencies                            |
+| `npm run dev`          | Starts local dev server at `localhost:3000`      |
+| `npm run build`        | Build your production site to `./dist/`          |
+| `npm run preview`      | Preview your build locally, before deploying     |
+| `npm run astro ...`    | Run CLI commands like `astro add`, `astro check` |
+| `npm run astro --help` | Get help using the Astro CLI                     |
+
+## 👀 Want to learn more?
+
+Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
diff --git a/examples/suspense/astro.config.mjs b/examples/suspense/astro.config.mjs
new file mode 100644
index 000000000..97e31f681
--- /dev/null
+++ b/examples/suspense/astro.config.mjs
@@ -0,0 +1,11 @@
+import { defineConfig } from 'astro/config';
+
+import node from "@astrojs/node";
+
+// https://astro.build/config
+export default defineConfig({
+  output: "server",
+  adapter: node({
+    mode: "standalone"
+  })
+});
\ No newline at end of file
diff --git a/examples/suspense/package.json b/examples/suspense/package.json
new file mode 100644
index 000000000..478c4131d
--- /dev/null
+++ b/examples/suspense/package.json
@@ -0,0 +1,17 @@
+{
+  "name": "@example/suspense",
+  "type": "module",
+  "version": "0.0.1",
+  "private": true,
+  "scripts": {
+    "dev": "astro dev",
+    "start": "astro dev",
+    "build": "astro build",
+    "preview": "astro preview",
+    "astro": "astro"
+  },
+  "dependencies": {
+    "@astrojs/node": "^5.1.0",
+    "astro": "^2.1.3"
+  }
+}
diff --git a/examples/suspense/public/favicon.svg b/examples/suspense/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/suspense/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+    <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+    <style>
+        path { fill: #000; }
+        @media (prefers-color-scheme: dark) {
+            path { fill: #FFF; }
+        }
+    </style>
+</svg>
diff --git a/examples/suspense/sandbox.config.json b/examples/suspense/sandbox.config.json
new file mode 100644
index 000000000..9178af77d
--- /dev/null
+++ b/examples/suspense/sandbox.config.json
@@ -0,0 +1,11 @@
+{
+  "infiniteLoopProtection": true,
+  "hardReloadOnChange": false,
+  "view": "browser",
+  "template": "node",
+  "container": {
+    "port": 3000,
+    "startScript": "start",
+    "node": "14"
+  }
+}
diff --git a/examples/suspense/src/components/Delay.astro b/examples/suspense/src/components/Delay.astro
new file mode 100644
index 000000000..3c5aee7c5
--- /dev/null
+++ b/examples/suspense/src/components/Delay.astro
@@ -0,0 +1,7 @@
+---
+import { setTimeout as sleep } from 'node:timers/promises';
+
+await sleep(Math.random() * 5000);
+---
+
+<h2><slot /></h2>
diff --git a/examples/suspense/src/components/Greeting.astro b/examples/suspense/src/components/Greeting.astro
new file mode 100644
index 000000000..103328ab8
--- /dev/null
+++ b/examples/suspense/src/components/Greeting.astro
@@ -0,0 +1,55 @@
+---
+import Delay from './Delay.astro';
+import Suspense from './Suspense.js';
+---
+
+<div>
+	<Suspense>
+		<h2
+			class="skeleton-box"
+			style={{ '--width': `${10 + Math.floor(Math.random() * 30)}%` }}
+			slot="fallback"
+		>
+		</h2>
+		<Delay><slot /></Delay>
+	</Suspense>
+</div>
+
+<style>
+	h2 {
+		width: 100%;
+	}
+	.skeleton-box {
+		display: inline-block;
+		height: 1em;
+		width: var(--width);
+		position: relative;
+		overflow: hidden;
+		background-color: #ddd;
+		color: transparent;
+		margin: 0;
+	}
+	.skeleton-box::after {
+		position: absolute;
+		top: 0;
+		right: 0;
+		bottom: 0;
+		left: 0;
+		transform: translateX(-100%);
+		background-image: linear-gradient(
+			to right,
+			rgb(255 255 255 / 0) 0%,
+			rgb(255 255 255 / 20%) 20%,
+			rgb(255 255 255 / 50%) 60%,
+			rgb(255 255 255 / 0) 100%
+		);
+		animation: shimmer 1s infinite;
+		content: '';
+	}
+
+	@keyframes shimmer {
+		100% {
+			transform: translateX(100%);
+		}
+	}
+</style>
diff --git a/examples/suspense/src/components/Suspense.js b/examples/suspense/src/components/Suspense.js
new file mode 100644
index 000000000..ffb897ea7
--- /dev/null
+++ b/examples/suspense/src/components/Suspense.js
@@ -0,0 +1,17 @@
+import { createComponent, render, renderSlot, renderSuspense } from 'astro/runtime/server/index.js';
+
+let ids = new WeakMap();
+export default createComponent({
+	factory(result, props, slots) {
+		const id = (ids.get(result) ?? -1) + 1;
+		ids.set(result, id);
+		let suspense = { status: 'pending' }
+		suspense.value = renderSuspense(result, id, slots.default).then((result) => {
+			suspense.status = 'fulfilled';
+			return result;
+		})
+		result.suspense.set(id, suspense);
+
+		return render`<astro-placeholder uid="${id}">${renderSlot(result, slots.fallback)}</astro-placeholder>`
+	}
+})
diff --git a/examples/suspense/src/env.d.ts b/examples/suspense/src/env.d.ts
new file mode 100644
index 000000000..f964fe0cf
--- /dev/null
+++ b/examples/suspense/src/env.d.ts
@@ -0,0 +1 @@
+/// <reference types="astro/client" />
diff --git a/examples/suspense/src/pages/index.astro b/examples/suspense/src/pages/index.astro
new file mode 100644
index 000000000..417032ba9
--- /dev/null
+++ b/examples/suspense/src/pages/index.astro
@@ -0,0 +1,43 @@
+---
+import Greeting from '../components/Greeting.astro';
+---
+
+<html lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+		<meta name="viewport" content="width=device-width" />
+		<meta name="generator" content={Astro.generator} />
+		<title>Astro</title>
+	</head>
+	<body>
+		<h1>Astro</h1>
+		<main>
+			<Greeting>Hello world</Greeting>
+			<Greeting>Hola mundo</Greeting>
+			<Greeting>Bonjour le monde</Greeting>
+			<Greeting>Hallo Welt</Greeting>
+		</main>
+	</body>
+</html>
+
+<style>
+	:global(*) {
+		margin: 0;
+		padding: 0;
+		box-sizing: border-box;
+	}
+
+	body {
+		padding: 1rem;
+		font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
+			Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+	}
+
+	main {
+		display: flex;
+		flex-direction: column;
+		padding-top: 2rem;
+		gap: 1rem;
+	}
+</style>
diff --git a/examples/suspense/tsconfig.json b/examples/suspense/tsconfig.json
new file mode 100644
index 000000000..d78f81ec4
--- /dev/null
+++ b/examples/suspense/tsconfig.json
@@ -0,0 +1,3 @@
+{
+  "extends": "astro/tsconfigs/base"
+}
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 2d4bcfa15..483fc5950 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -1588,6 +1588,7 @@ export interface SSRResult {
 	): AstroGlobal;
 	resolve: (s: string) => Promise<string>;
 	response: ResponseInit;
+	suspense: Map<string, { status: 'pending' | 'fulfilled' | 'rendered', value: any | Promise<any> }>;
 	// Bits 1 = astro, 2 = jsx, 4 = slot
 	// As rendering occurs these bits are manipulated to determine where content
 	// is within a slot. This is used for head injection.
diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts
index f1474a0a3..bb08548e0 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -160,6 +160,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
 		links: args.links ?? new Set<SSRElement>(),
 		propagation: args.propagation ?? new Map(),
 		propagators: new Map(),
+		suspense: new Map(),
 		extraHead: [],
 		scope: 0,
 		cookies,
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index cae48fd41..6eea21eb5 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -21,6 +21,7 @@ export {
 	renderPage,
 	renderScriptElement,
 	renderSlot,
+	renderSuspense,
 	renderStyleElement,
 	renderTemplate as render,
 	renderTemplate,
diff --git a/packages/astro/src/runtime/server/render/astro/instance.ts b/packages/astro/src/runtime/server/render/astro/instance.ts
index 9a0839e51..f217430b4 100644
--- a/packages/astro/src/runtime/server/render/astro/instance.ts
+++ b/packages/astro/src/runtime/server/render/astro/instance.ts
@@ -8,6 +8,7 @@ import { renderChild } from '../any.js';
 import { createScopedResult, ScopeFlags } from '../scope.js';
 import { isAPropagatingComponent } from './factory.js';
 import { isHeadAndContent } from './head-and-content.js';
+import { markHTMLString } from '../../escape.js';
 
 type ComponentProps = Record<string | number, any>;
 
diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts
index 76faffdbf..ae1ffe63b 100644
--- a/packages/astro/src/runtime/server/render/common.ts
+++ b/packages/astro/src/runtime/server/render/common.ts
@@ -21,7 +21,7 @@ export const decoder = new TextDecoder();
 // Rendering produces either marked strings of HTML or instructions for hydration.
 // These directive instructions bubble all the way up to renderPage so that we
 // can ensure they are added only once, and as soon as possible.
-export function stringifyChunk(result: SSRResult, chunk: string | SlotString | RenderInstruction) {
+export function stringifyChunk(result: SSRResult, chunk: string | SlotString | RenderInstruction): string {
 	if (typeof (chunk as any).type === 'string') {
 		const instruction = chunk as RenderInstruction;
 		switch (instruction.type) {
@@ -43,6 +43,12 @@ export function stringifyChunk(result: SSRResult, chunk: string | SlotString | R
 					return '';
 				}
 			}
+			case 'suspense': {
+				const { id } = instruction;
+				const content = stringifyChunk(result, instruction.content);
+				const html = JSON.stringify(content).replace(/\<\/script\>/g, `</" + "script>`);
+				return markHTMLString(`<script async defer>document.querySelector("astro-placeholder[uid='${id}']").outerHTML = ${html};document.currentScript.remove();</script>`);
+			}
 			case 'head': {
 				if (result._metadata.hasRenderedHead) {
 					return '';
diff --git a/packages/astro/src/runtime/server/render/index.ts b/packages/astro/src/runtime/server/render/index.ts
index 4f7e14c9d..94bc6f7f8 100644
--- a/packages/astro/src/runtime/server/render/index.ts
+++ b/packages/astro/src/runtime/server/render/index.ts
@@ -12,6 +12,7 @@ export { maybeRenderHead, renderHead } from './head.js';
 export { renderPage } from './page.js';
 export { addScopeFlag, createScopedResult, removeScopeFlag, ScopeFlags } from './scope.js';
 export { renderSlot, type ComponentSlots } from './slot.js';
+export { renderSuspense } from './suspense.js';
 export { renderScriptElement, renderStyleElement, renderUniqueStylesheet } from './tags.js';
 export type { RenderInstruction } from './types';
 export { addAttribute, defineScriptVars, voidElementNames } from './util.js';
diff --git a/packages/astro/src/runtime/server/render/page.ts b/packages/astro/src/runtime/server/render/page.ts
index 857cfdd6a..fd42c08c0 100644
--- a/packages/astro/src/runtime/server/render/page.ts
+++ b/packages/astro/src/runtime/server/render/page.ts
@@ -153,6 +153,9 @@ export async function renderPage(
 								controller.enqueue(bytes);
 								i++;
 							}
+
+							await Promise.all(Array.from(result.suspense.values()).map(suspense => suspense.value.then((chunk: any) => controller.enqueue(chunkToByteArray(result, chunk)))))
+
 							controller.close();
 						} catch (e) {
 							// We don't have a lot of information downstream, and upstream we can't catch the error properly
diff --git a/packages/astro/src/runtime/server/render/suspense.ts b/packages/astro/src/runtime/server/render/suspense.ts
new file mode 100644
index 000000000..12c25acc6
--- /dev/null
+++ b/packages/astro/src/runtime/server/render/suspense.ts
@@ -0,0 +1,18 @@
+import type { SSRResult } from '../../../@types/astro.js';
+import type { RenderInstruction } from './types.js';
+
+import { markHTMLString } from '../escape.js';
+import { renderSlot } from './slot.js';
+
+export async function renderSuspense(
+	result: SSRResult,
+	id: string,
+	slotted: any,
+) {
+	if (slotted) {
+		const content = await renderSlot(result, slotted);
+		return { type: 'suspense', id, content }
+	}
+
+	return '';
+}
diff --git a/packages/astro/src/runtime/server/render/types.ts b/packages/astro/src/runtime/server/render/types.ts
index 31702f444..808e61082 100644
--- a/packages/astro/src/runtime/server/render/types.ts
+++ b/packages/astro/src/runtime/server/render/types.ts
@@ -7,6 +7,13 @@ export type RenderDirectiveInstruction = {
 	hydration: HydrationMetadata;
 };
 
+export type RenderSuspenseInstruction = {
+	type: 'suspense';
+	result: SSRResult;
+	id: string;
+	content: any;
+};
+
 export type RenderHeadInstruction = {
 	type: 'head';
 	result: SSRResult;
@@ -20,5 +27,6 @@ export type MaybeRenderHeadInstruction = {
 
 export type RenderInstruction =
 	| RenderDirectiveInstruction
+	| RenderSuspenseInstruction
 	| RenderHeadInstruction
 	| MaybeRenderHeadInstruction;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 44e6f5467..e5cd32afe 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -326,6 +326,14 @@ importers:
       unocss: 0.15.6
       vite-imagetools: 4.0.18
 
+  examples/suspense:
+    specifiers:
+      '@astrojs/node': ^5.1.0
+      astro: ^2.1.3
+    dependencies:
+      '@astrojs/node': link:../../packages/integrations/node
+      astro: link:../../packages/astro
+
   examples/with-markdoc:
     specifiers:
       '@astrojs/markdoc': ^0.0.2
@@ -4194,14 +4202,15 @@ packages:
       - react
     dev: false
 
-  /@astrojs/markdown-remark/2.0.1_astro@packages+astro:
-    resolution: {integrity: sha512-xQF1rXGJN18m+zZucwRRtmNehuhPMMhZhi6HWKrtpEAKnHSPk8lqf1GXgKH7/Sypglu8ivdECZ+EGs6kOYVasQ==}
+  /@astrojs/markdown-remark/2.1.0_astro@packages+astro:
+    resolution: {integrity: sha512-w9T5o3UWQIfMcCkM2nLWrlfVQazh/7mw+2N/85QGcSUkZy6oNJoyy8Xz/ZkDhHLx8HPO0RT9fABR0B/H+aDaEw==}
     peerDependencies:
       astro: '*'
     dependencies:
-      '@astrojs/prism': 2.0.0
+      '@astrojs/prism': 2.1.1
       astro: link:packages/astro
       github-slugger: 1.5.0
+      image-size: 1.0.2
       import-meta-resolve: 2.2.1
       rehype-raw: 6.1.1
       rehype-stringify: 9.0.3
@@ -4221,8 +4230,8 @@ packages:
     resolution: {integrity: sha512-mol57cw1jJMcQgKMRGn7p6cewajq6JTNtqj5aAZgROWam/phVDSOCbXj/WU3O9+3qFnyKtpczoufQKwJTQltAw==}
     engines: {node: '>=16.12.0'}
     dependencies:
-      '@astrojs/markdown-remark': 2.0.1_astro@packages+astro
-      '@astrojs/prism': 2.0.0
+      '@astrojs/markdown-remark': 2.1.0_astro@packages+astro
+      '@astrojs/prism': 2.1.1
       '@mdx-js/mdx': 2.3.0
       '@mdx-js/rollup': 2.3.0
       acorn: 8.8.2
@@ -4266,8 +4275,8 @@ packages:
       - supports-color
     dev: false
 
-  /@astrojs/prism/2.0.0:
-    resolution: {integrity: sha512-YgeoeEPqsxaEpg0rwe/bUq3653LqSQnMjrLlpYwrbQQMQQqz6Y5yXN+RX3SfLJ6ppNb4+Fu2+Z49EXjk48Ihjw==}
+  /@astrojs/prism/2.1.1:
+    resolution: {integrity: sha512-Gnwnlb1lGJzCQEg89r4/WqgfCGPNFC7Kuh2D/k289Cbdi/2PD7Lrdstz86y1itDvcb2ijiRqjqWnJ5rsfu/QOA==}
     engines: {node: '>=16.12.0'}
     dependencies:
       prismjs: 1.29.0
@@ -15007,6 +15016,7 @@ packages:
   /source-map/0.6.1:
     resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
     engines: {node: '>=0.10.0'}
+    requiresBuild: true
 
   /source-map/0.7.4:
     resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}