diff --git a/.changeset/happy-parrots-stare.md b/.changeset/happy-parrots-stare.md
new file mode 100644
index 000000000000..54d117320b7f
--- /dev/null
+++ b/.changeset/happy-parrots-stare.md
@@ -0,0 +1,8 @@
+---
+'astro': minor
+'@astrojs/cloudflare': minor
+'@astrojs/netlify': minor
+'@astrojs/vercel': minor
+---
+
+Support for 404 and 500 pages in SSR
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
index 518c1fc582f8..7adde8820658 100644
--- a/packages/astro/src/core/app/index.ts
+++ b/packages/astro/src/core/app/index.ts
@@ -25,6 +25,10 @@ export { deserializeManifest } from './common.js';
 export const pagesVirtualModuleId = '@astrojs-pages-virtual-entry';
 export const resolvedPagesVirtualModuleId = '\0' + pagesVirtualModuleId;
 
+export interface MatchOptions {
+	matchNotFound?: boolean | undefined;
+}
+
 export class App {
 	#manifest: Manifest;
 	#manifestData: ManifestData;
@@ -46,17 +50,30 @@ export class App {
 		this.#routeCache = new RouteCache(this.#logging);
 		this.#streaming = streaming;
 	}
-	match(request: Request): RouteData | undefined {
+	match(request: Request, { matchNotFound = false }: MatchOptions = {}): RouteData | undefined {
 		const url = new URL(request.url);
 		// ignore requests matching public assets
 		if (this.#manifest.assets.has(url.pathname)) {
 			return undefined;
 		}
-		return matchRoute(url.pathname, this.#manifestData);
+		let routeData = matchRoute(url.pathname, this.#manifestData);
+
+		if(routeData) {
+			return routeData;
+		} else if(matchNotFound) {
+			return matchRoute('/404', this.#manifestData);
+		} else {
+			return undefined;
+		}
 	}
 	async render(request: Request, routeData?: RouteData): Promise<Response> {
+		let defaultStatus = 200;
 		if (!routeData) {
 			routeData = this.match(request);
+			if (!routeData) {
+				defaultStatus = 404;
+				routeData = this.match(request, { matchNotFound: true });
+			}
 			if (!routeData) {
 				return new Response(null, {
 					status: 404,
@@ -65,12 +82,25 @@ export class App {
 			}
 		}
 
-		const mod = this.#manifest.pageMap.get(routeData.component)!;
+		let mod = this.#manifest.pageMap.get(routeData.component)!;
 
 		if (routeData.type === 'page') {
-			return this.#renderPage(request, routeData, mod);
+			let response = await this.#renderPage(request, routeData, mod, defaultStatus);
+
+			// If there was a 500 error, try sending the 500 page.
+			if(response.status === 500) {
+				const fiveHundredRouteData = matchRoute('/500', this.#manifestData);
+				if(fiveHundredRouteData) {
+					mod = this.#manifest.pageMap.get(fiveHundredRouteData.component)!;
+					try {
+						let fiveHundredResponse = await this.#renderPage(request, fiveHundredRouteData, mod, 500);
+						return fiveHundredResponse;
+					} catch {}
+				}
+			}
+			return response;
 		} else if (routeData.type === 'endpoint') {
-			return this.#callEndpoint(request, routeData, mod);
+			return this.#callEndpoint(request, routeData, mod, defaultStatus);
 		} else {
 			throw new Error(`Unsupported route type [${routeData.type}].`);
 		}
@@ -79,7 +109,8 @@ export class App {
 	async #renderPage(
 		request: Request,
 		routeData: RouteData,
-		mod: ComponentInstance
+		mod: ComponentInstance,
+		status = 200
 	): Promise<Response> {
 		const url = new URL(request.url);
 		const manifest = this.#manifest;
@@ -128,6 +159,7 @@ export class App {
 				ssr: true,
 				request,
 				streaming: this.#streaming,
+				status
 			});
 
 			return response;
@@ -143,7 +175,8 @@ export class App {
 	async #callEndpoint(
 		request: Request,
 		routeData: RouteData,
-		mod: ComponentInstance
+		mod: ComponentInstance,
+		status = 200
 	): Promise<Response> {
 		const url = new URL(request.url);
 		const handler = mod as unknown as EndpointHandler;
@@ -155,6 +188,7 @@ export class App {
 			route: routeData,
 			routeCache: this.#routeCache,
 			ssr: true,
+			status
 		});
 
 		if (result.type === 'response') {
diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts
index 2e0318d58d46..117953dac505 100644
--- a/packages/astro/src/core/endpoint/index.ts
+++ b/packages/astro/src/core/endpoint/index.ts
@@ -5,7 +5,7 @@ import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js';
 
 export type EndpointOptions = Pick<
 	RenderOptions,
-	'logging' | 'origin' | 'request' | 'route' | 'routeCache' | 'pathname' | 'route' | 'site' | 'ssr'
+	'logging' | 'origin' | 'request' | 'route' | 'routeCache' | 'pathname' | 'route' | 'site' | 'ssr' | 'status'
 >;
 
 type EndpointCallResult =
diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts
index df8cb49d2765..94aa62ac837b 100644
--- a/packages/astro/src/core/render/core.ts
+++ b/packages/astro/src/core/render/core.ts
@@ -85,6 +85,7 @@ export interface RenderOptions {
 	ssr: boolean;
 	streaming: boolean;
 	request: Request;
+	status?: number;
 }
 
 export async function render(opts: RenderOptions): Promise<Response> {
@@ -107,6 +108,7 @@ export async function render(opts: RenderOptions): Promise<Response> {
 		site,
 		ssr,
 		streaming,
+		status = 200
 	} = opts;
 
 	const paramsAndPropsRes = await getParamsAndProps({
@@ -148,6 +150,7 @@ export async function render(opts: RenderOptions): Promise<Response> {
 		scripts,
 		ssr,
 		streaming,
+		status
 	});
 
 	// Support `export const components` for `MDX` pages
diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts
index a7f36ee7933b..6e5da1d699ce 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -42,6 +42,7 @@ export interface CreateResultArgs {
 	scripts?: Set<SSRElement>;
 	styles?: Set<SSRElement>;
 	request: Request;
+	status: number;
 }
 
 function getFunctionExpression(slot: any) {
@@ -119,7 +120,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
 		headers.set('Content-Type', 'text/html');
 	}
 	const response: ResponseInit = {
-		status: 200,
+		status: args.status,
 		statusText: 'OK',
 		headers,
 	};
diff --git a/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/404.astro b/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/404.astro
index 658f0ef9b57f..71a4a4d2cb99 100644
--- a/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/404.astro
+++ b/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/404.astro
@@ -1,5 +1 @@
----
-
----
-
-<h1>Something went horribly wrong!!</h1>
\ No newline at end of file
+<h1>Something went horribly wrong!</h1>
diff --git a/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/500.astro b/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/500.astro
index 28a3b7cc21bb..0e36085e2d47 100644
--- a/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/500.astro
+++ b/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/500.astro
@@ -1,3 +1 @@
----
-throw new Error(`oops`);
----
+<h1>This is an error page</h1>
diff --git a/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/causes-error.astro b/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/causes-error.astro
new file mode 100644
index 000000000000..28a3b7cc21bb
--- /dev/null
+++ b/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/causes-error.astro
@@ -0,0 +1,3 @@
+---
+throw new Error(`oops`);
+---
diff --git a/packages/astro/test/ssr-404-500-pages.test.js b/packages/astro/test/ssr-404-500-pages.test.js
new file mode 100644
index 000000000000..45d60d4baa16
--- /dev/null
+++ b/packages/astro/test/ssr-404-500-pages.test.js
@@ -0,0 +1,40 @@
+import { expect } from 'chai';
+import { loadFixture } from './test-utils.js';
+import testAdapter from './test-adapter.js';
+import * as cheerio from 'cheerio';
+
+describe('404 and 500 pages', () => {
+	/** @type {import('./test-utils').Fixture} */
+	let fixture;
+
+	before(async () => {
+		fixture = await loadFixture({
+			root: './fixtures/ssr-api-route-custom-404/',
+			experimental: {
+				ssr: true,
+			},
+			adapter: testAdapter(),
+		});
+		await fixture.build({  });
+	});
+
+	it('404 page returned when a route does not match', async () => {
+		const app = await fixture.loadTestAdapterApp();
+		const request = new Request('http://example.com/some/fake/route');
+		const response = await app.render(request);
+		expect(response.status).to.equal(404);
+		const html = await response.text();
+		const $ = cheerio.load(html);
+		expect($('h1').text()).to.equal('Something went horribly wrong!');
+	});
+
+	it('500 page returned when there is an error', async () => {
+		const app = await fixture.loadTestAdapterApp();
+		const request = new Request('http://example.com/causes-error');
+		const response = await app.render(request);
+		expect(response.status).to.equal(500);
+		const html = await response.text();
+		const $ = cheerio.load(html);
+		expect($('h1').text()).to.equal('This is an error page');
+	});
+});
diff --git a/packages/astro/test/ssr-api-route.test.js b/packages/astro/test/ssr-api-route.test.js
index 40d690f2dca8..ec6e65641434 100644
--- a/packages/astro/test/ssr-api-route.test.js
+++ b/packages/astro/test/ssr-api-route.test.js
@@ -1,12 +1,10 @@
 import { expect } from 'chai';
-import * as cheerio from 'cheerio';
 import { loadFixture } from './test-utils.js';
 import testAdapter from './test-adapter.js';
 
 describe('API routes in SSR', () => {
 	/** @type {import('./test-utils').Fixture} */
 	let fixture;
-	let errorFixtures;
 
 	before(async () => {
 		fixture = await loadFixture({
@@ -16,97 +14,61 @@ describe('API routes in SSR', () => {
 			},
 			adapter: testAdapter(),
 		});
-		errorFixtures = await loadFixture({
-			root: './fixtures/ssr-api-route-custom-404/',
-			experimental: {
-				ssr: true,
-			},
-			server: {
-				port: 5173
-			},
-			adapter: testAdapter(),
-		});
-		await errorFixtures.build();
 		await fixture.build();
 	});
 
-	// it('Basic pages work', async () => {
-	// 	const app = await fixture.loadTestAdapterApp();
-	// 	const request = new Request('http://example.com/');
-	// 	const response = await app.render(request);
-	// 	const html = await response.text();
-	// 	expect(html).to.not.be.empty;
-	// });
+	it('Basic pages work', async () => {
+		const app = await fixture.loadTestAdapterApp();
+		const request = new Request('http://example.com/');
+		const response = await app.render(request);
+		const html = await response.text();
+		expect(html).to.not.be.empty;
+	});
 
-	// it('Can load the API route too', async () => {
-	// 	const app = await fixture.loadTestAdapterApp();
-	// 	const request = new Request('http://example.com/food.json');
-	// 	const response = await app.render(request);
-	// 	expect(response.status).to.equal(200);
-	// 	expect(response.headers.get('Content-Type')).to.equal('application/json;charset=utf-8');
-	// 	expect(response.headers.get('Content-Length')).to.not.be.empty;
-	// 	const body = await response.json();
-	// 	expect(body.length).to.equal(3);
-	// });
+	it('Can load the API route too', async () => {
+		const app = await fixture.loadTestAdapterApp();
+		const request = new Request('http://example.com/food.json');
+		const response = await app.render(request);
+		expect(response.status).to.equal(200);
+		expect(response.headers.get('Content-Type')).to.equal('application/json;charset=utf-8');
+		expect(response.headers.get('Content-Length')).to.not.be.empty;
+		const body = await response.json();
+		expect(body.length).to.equal(3);
+	});
 
 	describe('API Routes - Dev', () => {
 		let devServer;
-		let errorDevServer;
 		before(async () => {
 			devServer = await fixture.startDevServer();
-			errorDevServer = await errorFixtures.startDevServer();
 		});
 
 		after(async () => {
 			await devServer.stop();
-			await errorDevServer.stop();
 		});
 
-		// 	it('Can POST to API routes', async () => {
-		// 		const response = await fixture.fetch('/food.json', {
-		// 			method: 'POST',
-		// 			body: `some data`,
-		// 		});
-		// 		expect(response.status).to.equal(200);
-		// 		const text = await response.text();
-		// 		expect(text).to.equal(`ok`);
-		// 	});
-
-		// 	it('Infer content type with charset for { body } shorthand', async () => {
-		// 		const response = await fixture.fetch('/food.json', {
-		// 			method: 'GET',
-		// 		});
-		// 		expect(response.headers.get('Content-Type')).to.equal('application/json;charset=utf-8');
-		// 	});
-
-		// 	it('Can set multiple headers of the same type', async () => {
-		// 		const response = await fixture.fetch('/login', {
-		// 			method: 'POST',
-		// 		});
-		// 		const setCookie = response.headers.get('set-cookie');
-		// 		expect(setCookie).to.equal('foo=foo; HttpOnly, bar=bar; HttpOnly');
-		// 	});
-
-		it('renders default 404 page for /404', async () => {
-			const html = await fixture.fetch('/404').then((res) => res.text());
-			const $ = cheerio.load(html);
-
-			expect($('h1').text()).to.equal('404:  Not found');
-			// expect($('p').text()).to.equal('/a/');
+		it('Can POST to API routes', async () => {
+			const response = await fixture.fetch('/food.json', {
+				method: 'POST',
+				body: `some data`,
+			});
+			expect(response.status).to.equal(200);
+			const text = await response.text();
+			expect(text).to.equal(`ok`);
 		});
 
-		// it('renders custom 404 page for /a', async () => {
-		// 	const html = await errorFixtures.fetch('/a').then((res) => res.text());
-		// 	const $ = cheerio.load(html);
-
-		// 	expect($('h1').text()).to.equal('Something went horribly wrong!!');
-		// });
-
-		// it('500 page for /500', async () => {
-		// 	const html = await fixture.fetch('/500').then((res) => res.text());
-		// 	const $ = cheerio.load(html);
+		it('Infer content type with charset for { body } shorthand', async () => {
+			const response = await fixture.fetch('/food.json', {
+				method: 'GET',
+			});
+			expect(response.headers.get('Content-Type')).to.equal('application/json;charset=utf-8');
+		});
 
-		// 	expect($('title').text().length).to.equal('Something went horribly wrong!!');
-		// });
+		it('Can set multiple headers of the same type', async () => {
+			const response = await fixture.fetch('/login', {
+				method: 'POST',
+			});
+			const setCookie = response.headers.get('set-cookie');
+			expect(setCookie).to.equal('foo=foo; HttpOnly, bar=bar; HttpOnly');
+		});
 	});
 });
diff --git a/packages/integrations/cloudflare/src/server.ts b/packages/integrations/cloudflare/src/server.ts
index 097f29d37cc5..7b88c7b1e4d1 100644
--- a/packages/integrations/cloudflare/src/server.ts
+++ b/packages/integrations/cloudflare/src/server.ts
@@ -19,19 +19,14 @@ export function createExports(manifest: SSRManifest) {
 			return env.ASSETS.fetch(assetRequest);
 		}
 
-		if (app.match(request)) {
+		let routeData = app.match(request, { matchNotFound: true });
+		if (routeData) {
 			Reflect.set(
 				request,
 				Symbol.for('astro.clientAddress'),
 				request.headers.get('cf-connecting-ip')
 			);
-			return app.render(request);
-		}
-
-		// 404
-		const _404Request = new Request(`${origin}/404`, request);
-		if (app.match(_404Request)) {
-			return app.render(_404Request);
+			return app.render(request, routeData);
 		}
 
 		return new Response(null, {
diff --git a/packages/integrations/netlify/src/netlify-functions.ts b/packages/integrations/netlify/src/netlify-functions.ts
index 0363fb8034a2..d40254f96eb8 100644
--- a/packages/integrations/netlify/src/netlify-functions.ts
+++ b/packages/integrations/netlify/src/netlify-functions.ts
@@ -66,7 +66,9 @@ export const createExports = (manifest: SSRManifest, args: Args) => {
 		}
 		const request = new Request(rawUrl, init);
 
-		if (!app.match(request)) {
+		let routeData = app.match(request, { matchNotFound: true });
+
+		if (!routeData) {
 			return {
 				statusCode: 404,
 				body: 'Not found',
@@ -76,7 +78,7 @@ export const createExports = (manifest: SSRManifest, args: Args) => {
 		const ip = headers['x-nf-client-connection-ip'];
 		Reflect.set(request, clientAddressSymbol, ip);
 
-		const response: Response = await app.render(request);
+		const response: Response = await app.render(request, routeData);
 		const responseHeaders = Object.fromEntries(response.headers.entries());
 
 		const responseContentType = parseContentType(responseHeaders['content-type']);
diff --git a/packages/integrations/vercel/src/serverless/entrypoint.ts b/packages/integrations/vercel/src/serverless/entrypoint.ts
index 852aebefda4a..6b94f201cc42 100644
--- a/packages/integrations/vercel/src/serverless/entrypoint.ts
+++ b/packages/integrations/vercel/src/serverless/entrypoint.ts
@@ -22,12 +22,13 @@ export const createExports = (manifest: SSRManifest) => {
 			return res.end(err.reason || 'Invalid request body');
 		}
 
-		if (!app.match(request)) {
+		let routeData = app.match(request, { matchNotFound: true });
+		if (!routeData) {
 			res.statusCode = 404;
 			return res.end('Not found');
 		}
 
-		await setResponse(res, await app.render(request));
+		await setResponse(res, await app.render(request, routeData));
 	};
 
 	return { default: handler };
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 633ee1b9da6f..8a6601fde461 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1740,6 +1740,12 @@ importers:
     dependencies:
       astro: link:../../..
 
+  packages/astro/test/fixtures/ssr-api-route-custom-404:
+    specifiers:
+      astro: workspace:*
+    dependencies:
+      astro: link:../../..
+
   packages/astro/test/fixtures/ssr-assets:
     specifiers:
       astro: workspace:*