Server-Side Rendering
Server-side rendering (SSR) tells Astro to build each page’s HTML on demand, at the moment a request arrives, rather than ahead of time at build. This is what you reach for when a page’s content depends on something Astro cannot know in advance: the logged-in user, a cookie, a query string, live inventory, or A/B-test buckets. The trade-off is straightforward — you give up the raw CDN speed of pre-rendered HTML in exchange for content that is always fresh and request-specific. Astro keeps its zero-JS-by-default philosophy intact here: SSR still ships only the HTML plus whatever island JavaScript you explicitly opt into.
Enabling on-demand rendering
Astro renders to static HTML by default. To unlock SSR you set the output mode in astro.config.mjs and install an adapter — a small package that targets a specific server runtime (Node, Cloudflare, Vercel, Netlify, Deno, and others). The adapter is what knows how to turn an incoming HTTP request into a rendered response on your host.
The fastest way to add both at once is the astro add command:
npx astro add node
That installs @astrojs/node and wires it into your config. The result looks like this:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
});
With output: 'server', every route is rendered on demand by default. You can still opt individual pages back into static pre-rendering with export const prerender = true — letting you mix static marketing pages and dynamic dashboards in one project.
In Astro 5 the older
output: 'hybrid'mode was removed. Useoutput: 'static'(the default) with per-pageexport const prerender = falseto make only some routes dynamic, oroutput: 'server'withexport const prerender = trueto make only some routes static.
Reading the request
Inside any .astro page or endpoint, Astro.request is the standard web Request object, and helpers like Astro.url, Astro.params, and Astro.cookies give you ergonomic access to the request data you need to personalize the response.
---
// src/pages/dashboard.astro
const token = Astro.cookies.get('session')?.value;
if (!token) {
return Astro.redirect('/login', 302);
}
const user = await fetch('https://api.example.com/me', {
headers: { Authorization: `Bearer ${token}` },
}).then((res) => res.json());
---
<h1>Welcome back, {user.name}</h1>
<p>You have {user.unreadCount} unread messages.</p>
Because this code runs per request, the component script executes fresh every time. Astro.redirect() short-circuits rendering and returns a redirect response — ideal for auth gates.
Dynamic routes on demand
SSR pairs naturally with dynamic route params. Unlike static dynamic routes, you do not need getStaticPaths() — the route matches any value and resolves it at request time.
---
// src/pages/products/[slug].astro
const { slug } = Astro.params;
const res = await fetch(`https://api.example.com/products/${slug}`);
if (res.status === 404) {
return new Response('Not found', { status: 404 });
}
const product = await res.json();
---
<h1>{product.name}</h1>
<p>{product.inStock ? 'In stock' : 'Sold out'}</p>
Controlling caching and headers
A rendered response is a real Response, so you can set headers — including cache directives — to let a CDN cache dynamic output for a short window. This gives you the freshness of SSR with much of the performance of static pages.
---
// src/pages/feed.astro
Astro.response.headers.set(
'Cache-Control',
'public, max-age=0, s-maxage=60, stale-while-revalidate=300'
);
const posts = await fetch('https://api.example.com/posts').then((r) => r.json());
---
<ul>{posts.map((p) => <li>{p.title}</li>)}</ul>
Output:
HTTP/1.1 200 OK
content-type: text/html
cache-control: public, max-age=0, s-maxage=60, stale-while-revalidate=300
SSR vs. static at a glance
| Concern | Static (SSG) | On-demand (SSR) |
|---|---|---|
| When HTML is built | At build time | Per request |
| Personalization | No | Yes (cookies, headers, auth) |
| Requires an adapter | No | Yes |
| Hosting | Any CDN / static host | Server runtime / serverless |
| Best for | Blogs, docs, marketing | Dashboards, carts, feeds |
getStaticPaths needed | Yes, for dynamic routes | No |
Islands still work the same
SSR does not change Astro’s component model. The page is rendered to HTML on the server, and any interactive islands you mark with a client:* directive hydrate in the browser exactly as they would in static mode — keeping the JavaScript payload minimal.
---
import Cart from '../components/Cart.tsx';
const items = await getCartItems(Astro.cookies.get('cart')?.value);
---
<Cart items={items} client:load />
Best practices
- Default to static and opt individual routes into SSR only when they genuinely need per-request data — most pages do not.
- Lean on
s-maxageandstale-while-revalidateso a CDN absorbs traffic spikes even on dynamic routes. - Keep secrets and tokens server-side; never leak them into props passed to client islands.
- Return proper status codes (
404,302,500) viaResponseandAstro.redirect()instead of rendering an error-looking page with a200. - Choose the adapter that matches your real deployment target early — switching runtimes later can surface runtime-specific API differences.
- Validate and sanitize
Astro.params, query strings, and cookies before using them in fetches or queries.