Skip to content
Astro as rendering 4 min read

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. Use output: 'static' (the default) with per-page export const prerender = false to make only some routes dynamic, or output: 'server' with export const prerender = true to 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

ConcernStatic (SSG)On-demand (SSR)
When HTML is builtAt build timePer request
PersonalizationNoYes (cookies, headers, auth)
Requires an adapterNoYes
HostingAny CDN / static hostServer runtime / serverless
Best forBlogs, docs, marketingDashboards, carts, feeds
getStaticPaths neededYes, for dynamic routesNo

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-maxage and stale-while-revalidate so 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) via Response and Astro.redirect() instead of rendering an error-looking page with a 200.
  • 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.
Last updated June 14, 2026
Was this helpful?