
What is the difference between static and on-demand rendering in Astro?
The difference between static and on-demand rendering in Astro is when the HTML is created. With static rendering (also called prerendering), Astro runs your page code once during astro build and writes a finished .html file that every visitor receives. With on-demand rendering (also called server-side rendering or SSR), the page code runs on a server for every request, so the HTML can depend on cookies, headers, the URL or fresh data. Static is Astro's default and needs nothing extra; on-demand rendering requires an adapter and is enabled per page with export const prerender = false, or for the whole site with output: 'server'.
Both modes use the same .astro components and the same routing, so choosing between them is about where and when code runs, not how you write it. In this article you'll see the two modes side by side with real build output, what each one can and can't do, how they differ in performance, cost and hosting, how to mix them in one project, and a practical guide to choosing. All examples were built with Astro 7.4.2 and the @astrojs/node 11.1.9 adapter.
The Same Page, Two Ways
Here's a page that uses Astro's default, static rendering:
---
// src/pages/index.astro
const builtAt = new Date().toISOString();
---
<h1>Home</h1>
<p>Built at {builtAt}</p>
And one that opts in to on-demand rendering:
---
// src/pages/now.astro
export const prerender = false;
const now = new Date().toISOString();
const userAgent = Astro.request.headers.get("user-agent") ?? "unknown";
---
<h1>Rendered on demand</h1>
<p>Time: {now}</p>
<p>User agent: {userAgent}</p>
After building and starting the server, I requested each page a few times:
<h1>Home</h1><p>Built at 2026-10-24T09:45:18.690Z</p>
<h1>Rendered on demand</h1><p>Time: 2026-10-24T09:45:19.816Z</p><p>User agent: curl-test</p>
<h1>Rendered on demand</h1><p>Time: 2026-10-24T09:45:20.866Z</p><p>User agent: curl-test</p>
The static page shows the moment the build ran, and it will show that same time until the next build. The on-demand page shows a new time on every request, and it can read the request's User-Agent header, which simply doesn't exist at build time.
What the Build Produces
The difference is visible in the dist/ folder.
A Fully Static Site
Without an adapter and without any on-demand pages, astro build writes plain files:
dist/
├── index.html
├── about/index.html
├── favicon.svg
└── _astro/ (bundled CSS, JS, images)
You can upload that folder to any static host or CDN. There's no server code at all.
A Site with On-Demand Pages
With the Node adapter and one or more prerender = false pages, the build log reported:
[build] output: "static"
[build] mode: "server"
[build] adapter: @astrojs/node
The output is split in two:
dist/
├── client/
│ ├── index.html (still prerendered)
│ └── favicon.svg
└── server/
├── entry.mjs (the server entry point)
└── chunks/
├── now_FLZJvguB.mjs
├── visits_CT6ZV7gP.mjs
└── ...
The prerendered homepage is still a plain HTML file in dist/client/. The on-demand pages became JavaScript modules in dist/server/, which the adapter runs when a request comes in. Note the log line: output stays "static" because that's the default for pages, while mode: "server" tells you a server is part of the result.
Side-by-Side Comparison
| Static (prerendered) | On-demand (SSR) | |
|---|---|---|
| When HTML is generated | Once, at build time | On every request |
| Default in Astro | Yes | No, opt in per page or with output: 'server' |
| Adapter required | No | Yes |
| Hosting | Any static host or CDN | A server, serverless function or edge runtime |
| Data freshness | As fresh as the last build | Live |
| Request headers and cookies | Not available | Available |
| Set status codes and response headers | No (host config only) | Yes, via Astro.response or a Response |
| Dynamic routes | Need getStaticPaths() | Read Astro.params directly |
| Time to first byte | Very fast, served from cache | Depends on server work and data fetching |
| Cost at scale | Close to zero | Compute per request |
| Failure modes | Build fails, old version stays live | Runtime errors affect live requests |
What Only On-Demand Rendering Can Do
Some features depend on there being a request to respond to. These only work on pages and endpoints rendered on demand:
- Reading request data:
Astro.request.headers, the request body for form posts, and query strings that differ per visitor. - Cookies:
Astro.cookies.get()andAstro.cookies.set(), for example for sessions, preferences or a visit counter. - Custom responses: setting
Astro.response.statusor headers, returning aResponseobject, or callingAstro.redirect(). - Sessions and actions that accept form submissions, which rely on a server.
- Unbounded dynamic routes: a product page for any ID in a database, without listing every ID at build time.
Here's a page that uses several of these at once:
---
// src/pages/product/[id].astro
export const prerender = false;
const products: Record<string, { name: string; available: boolean }> = {
"1": { name: "Desk lamp", available: true },
"2": { name: "Old chair", available: false },
};
const product = products[Astro.params.id ?? ""];
if (!product) {
return new Response("Not found", { status: 404 });
}
if (!product.available) {
return Astro.redirect("/products", 301);
}
Astro.response.headers.set("Cache-Control", "public, max-age=60");
---
<h1>{product.name}</h1>
Requesting it with curl -i gave these responses:
| Request | Status | Notable header |
|---|---|---|
/product/1 | 200 OK | cache-control: public, max-age=60 |
/product/2 | 301 Moved Permanently | location: /products |
/product/9 | 404 Not Found | none |
There's no getStaticPaths(): the page reads Astro.params.id at request time. A static version would need to list every product during the build, as described in the post on dynamic routes with getStaticPaths.
What Happens If You Try It on a Static Page
If a prerendered page reads request headers, Astro builds it anyway but warns you:
[WARN] `Astro.request.headers` was used when rendering the route `src/pages/static-headers.astro'`. `Astro.request.headers` is not available on prerendered pages.
The page in my test rendered with an empty value, because there was no real request during the build. Watch the build log for this warning, since it usually means a page should be rendered on demand.
What Static Rendering Does Better
On-demand rendering is more capable, but static pages win on almost every operational measure:
- Speed: a static file can be served from a CDN edge close to the visitor, with no code running. On-demand pages pay for server work and any data fetching on every uncached request.
- Cost: static hosting is often free or nearly free. On-demand rendering uses compute for each request.
- Reliability: if your database or API goes down, static pages keep working. A broken build doesn't take the live site down; the previous deploy stays up.
- Simplicity: no adapter, no runtime, no server logs to watch.
This is why the Astro docs recommend starting with the default static mode and only rendering on demand where you need it.
Mixing Both in One Project
You rarely have to choose one mode for an entire site. Astro decides per route:
- Default (
output: 'static'): every page is prerendered unless it exportsprerender = false. Best when most of the site is content. output: 'server': every page is rendered on demand unless it exportsprerender = true. Best when most pages are personalised, such as an app behind a login.
The 'server' setting doesn't unlock any extra features; it only flips the default. A typical content site keeps the default and marks a handful of routes, such as a search results page, a dashboard or an API endpoint, as on-demand. That pattern is covered in detail in the post on mixing prerendered and on-demand pages.
There's also a middle ground at the component level. With server islands, a page stays static while one component, such as a user avatar or a personalised price, is rendered on demand and loaded after the page with server:defer. Server islands still need an adapter installed.
Making On-Demand Pages Fast
If you do render on demand, you can recover much of the static speed advantage:
- HTML streaming: Astro streams on-demand pages in order, so the browser can start rendering the top of the page while slower components are still fetching data.
- HTTP caching: set
Cache-Controlheaders so a CDN can serve repeat requests without hitting your server. - Route caching: Astro 7 adds a stable, platform-agnostic
cacheconfig withrouteRulesand anAstro.cacheAPI, backed by an in-memory provider or a CDN provider from your adapter. It's explained in the post on caching on-demand pages.
How to Choose
Ask these questions for each page:
- Does the HTML depend on who is asking? Logged-in state, cookies, location or headers mean on-demand.
- Does the data change more often than you can rebuild? Stock levels or live scores point to on-demand, or a static page with a small server island.
- Is the set of URLs unknown or huge? Millions of product IDs are a good fit for on-demand; a few hundred blog posts are not.
- Does it need to accept form posts or set cookies? That needs a server.
- Otherwise, prerender it.
A few examples:
| Page | Recommended mode |
|---|---|
| Blog post, docs page, landing page | Static |
| Blog index with pagination | Static |
| Search results based on a query string | On-demand |
| Account dashboard | On-demand |
| Product page with live stock | Static page plus server island, or on-demand with caching |
| Contact form handler | On-demand endpoint or action |
| RSS feed and sitemap | Static |
FAQ: Static vs On-Demand Rendering
Yes. Every page, route and endpoint is prerendered at build time unless you opt out with export const prerender = false or set output to server in your Astro config.
Yes. The Astro docs use on-demand rendering and server-side rendering to describe the same thing: HTML generated by a server at request time rather than at build time.
Yes. Without one, the build stops with a NoAdapterInstalled error. Adapters exist for Node.js, Netlify, Vercel and Cloudflare, plus community adapters for other runtimes.
Yes. Static only describes how the HTML is generated. Static pages can still include client-side scripts and hydrated framework components that run in the browser.
Yes, at build time. Top-level await in the frontmatter runs during the build, and the result is baked into the HTML until the next build.
It can. Every page is then rendered per request unless it opts back in with prerender = true. Keep fully static pages prerendered so they can be served directly from a CDN.
Conclusion
Static and on-demand rendering in Astro differ in one way that drives everything else: static pages run your code once at build time and ship finished HTML, while on-demand pages run it for every request on a server. Static pages are faster, cheaper and harder to break; on-demand pages can read cookies and headers, set status codes, redirect and show live data.
Because Astro decides per route, you don't have to pick one for the whole site. Keep the default static mode, add an adapter, and mark only the pages that truly depend on the request with export const prerender = false. For partly dynamic pages, server islands and caching let you keep most of the static performance while still showing fresh content.


