
What is server islands in Astro and when to use it?
A server island in Astro is an Astro component marked with the server:defer directive, which tells Astro to render it on the server separately from the rest of the page. The page itself is sent immediately, often as fully static, cacheable HTML, with optional fallback content where the island goes. The browser then fetches the island's HTML from a special endpoint and swaps it in. Use server islands when most of a page is the same for everyone but a few parts are personalised or slow to compute: a logged-in user's avatar, a cart count, live stock levels, recommendations or reviews. They need an adapter, because the island is rendered on demand.
Client islands, which hydrate framework components in the browser, solve the problem of interactivity. Server islands solve a different one: dynamic content that would otherwise force the whole page to be rendered on every request. In this article you'll learn how server islands work, how to add one with fallback content, what actually happens in the browser, how props, caching and URLs behave, how to handle encryption keys across deployments, and how to decide between a server island, a client island and an on-demand page.
The Problem Server Islands Solve
Imagine a product page. The product name, photos, description and price change rarely. The header shows the visitor's name and avatar, and a box under the price shows whether the item is in stock at their nearest store.
Without server islands you have two unattractive choices:
- Render the page on every request: the personalised parts are correct, but every visitor waits for the slowest database call, and the page can't be cached as a whole.
- Prerender the page and fetch personal data in the browser: the page is fast, but you need client-side JavaScript, an API endpoint and loading states for each personalised part.
Server islands give you a third option. The page is prerendered (or cached) once for everyone, and each personalised part is rendered by the server in its own request, using normal Astro component code with access to cookies, headers and your database. The concepts behind both kinds of island are covered in Astro's island architecture.
Requirements
Server islands are rendered on demand, so the project needs a server adapter, such as @astrojs/node, @astrojs/vercel, @astrojs/netlify or @astrojs/cloudflare:
npx astro add node
The page containing the island doesn't have to be on-demand rendered. A prerendered page can contain server islands; only the islands themselves run per request. Adapters and rendering modes are compared in static vs on-demand rendering.
Creating a Server Island
Any Astro component can become a server island. Here's one that reads a cookie, sets a cache header and, for demonstration, prints its own URL:
---
// src/components/UserGreeting.astro
interface Props {
productId: string;
}
const { productId } = Astro.props;
const name = Astro.cookies.get('name')?.value ?? 'guest';
Astro.response.headers.set('Cache-Control', 'private, max-age=60');
---
<p>Welcome back, {name}. Ready to buy product {productId}?</p>
<p>Island URL: {Astro.url.pathname}</p>
Nothing in the component itself says "island". It's an ordinary component that uses request data. The directive goes where you use it:
---
// src/pages/product.astro
import UserGreeting from '../components/UserGreeting.astro';
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Product</title>
</head>
<body>
<h1>Blue mug</h1>
<UserGreeting productId="mug-01" server:defer>
<p slot="fallback">Loading your account...</p>
</UserGreeting>
</body>
</html>
Two parts matter:
- server:defer: turns this use of the component into a server island.
- slot="fallback": content shown immediately in the island's place until the real HTML arrives. It can be any HTML or component, such as a generic avatar, a skeleton or a short message.
In my test project this page has no prerender = false, so it's built to static HTML at build time. The island still renders per request.
What Happens in the Browser
Here's the relevant part of the built HTML for that page, with the long encrypted strings shortened:
<link rel="preload" as="fetch" href="/_server-islands/UserGreeting?e=2CA1...&p=16FC...&s=" crossorigin="anonymous">
...
<h1>Blue mug</h1>
<script>async function replaceServerIsland(id, r) { ... }</script>
<!--[if astro]>server-island-start<![endif]--><p>Loading your account...</p>
<script type="module" data-astro-rerun data-island-id="d3352eef-...">const headers = new Headers({});
let response = await fetch("/_server-islands/UserGreeting?e=2CA1...&p=16FC...&s=", { headers });replaceServerIsland("d3352eef-...", response);</script>
At build time Astro removed the component, left the fallback in its place, and added a small script. Each island becomes its own route under /_server-islands/. When the page loads:
- The
<link rel="preload">in the head starts the island request early, often before the page has finished painting. - The visitor sees the static page with the fallback content.
- The inline script fetches the island's URL, which includes the component's props in encrypted form.
- The server renders the component and returns plain HTML.
- The script replaces the fallback with that HTML.
I requested the island URL directly with a cookie to see the response (a few connection headers removed):
curl -i -H 'Cookie: name=Sajjad' "http://127.0.0.1:4805/_server-islands/UserGreeting?e=...&p=...&s="
HTTP/1.1 200 OK
cache-control: private, max-age=60
content-type: text/html
x-robots-tag: noindex
<p>Welcome back, Sajjad. Ready to buy product mug-01?</p><p>Island URL: /_server-islands/UserGreeting</p>
The Cache-Control header set inside the component made it onto the island response. Each island loads independently, so a slow island doesn't hold up the others on the page.
Note the x-robots-tag: noindex header. Island endpoints aren't meant to be indexed on their own, and content that only appears through an island arrives via JavaScript after the page loads. Keep anything important for search, such as product names and descriptions, in the main page.
Passing Props to Server Islands
Props work, but with limits. They're serialised, encrypted and put in the island's URL, so they must be serialisable: plain objects, strings, numbers, arrays, Map, Set, Date, RegExp, BigInt, URL, typed arrays and Infinity. Functions and objects with circular references can't be passed.
Keep props small. Island data is requested with GET, which means the response can be cached by browsers and CDNs. If the encrypted props make the URL longer than 2,048 bytes, Astro switches to a POST request with the props in the body, and POST responses aren't cached. Pass an ID and let the island look up the rest:
<!-- Good: small props, cacheable GET -->
<ProductReviews productId={product.id} server:defer />
<!-- Risky: large props may push the request to POST -->
<ProductReviews product={product} reviews={allReviews} server:defer />
Reading the Page URL Inside an Island
Server islands run in their own request, not the page's. Inside the island, Astro.url and Astro.request.url point to the island's own route, which is why the test component above printed /_server-islands/UserGreeting rather than /product/.
If the island needs something from the page URL, such as a query parameter, either pass it as a prop or read the Referer header, which the browser sets to the page that loaded the island:
---
// src/components/Recommendations.astro
const referer = Astro.request.headers.get('Referer');
if (!referer) {
throw new Error('Referer header is missing');
}
const pageUrl = new URL(referer);
const category = pageUrl.searchParams.get('category');
---
<p>More from {category ?? 'all categories'}</p>
On a prerendered page, props can't include query parameters at all, because the props were fixed at build time. The Referer approach is the only way to react to them.
Caching Server Islands
Because islands are ordinary GET responses, you control caching with headers set inside the component:
- Personalised content: use
private, so shared caches like CDNs don't store one user's data for another, for exampleCache-Control: private, max-age=60. - Shared but dynamic content: such as stock levels or a "trending now" list, use a short public cache, for example
Cache-Control: public, max-age=30, stale-while-revalidate=60, so a CDN can absorb traffic.
Meanwhile the page around the islands can be cached aggressively, or served as a static file, because it contains nothing personal. That split is the main performance benefit of server islands. For caching whole on-demand pages, see caching on-demand pages.
Keeping the Encryption Key in Sync
Astro encrypts island props with a key that's generated fresh on every build and embedded in the server bundle. Normally the page and the server come from the same build, so they always match.
Problems appear when they don't, for example with rolling deployments where old and new servers run side by side, multi-region hosting, or a CDN still serving pages from a previous build. A page encrypted with the old key can't be decrypted by a server with the new one.
The fix is a fixed key. Generate one with the CLI:
npx astro create-key
[crypto] Generated a key to encrypt props passed to server islands. To reuse the same key across builds, set this value as ASTRO_KEY in an environment variable on your build server.
ASTRO_KEY=...
Set ASTRO_KEY in your build environment, and every build will use the same key. Treat it like any other secret.
When to Use Server Islands
| Situation | Best tool |
|---|---|
| Personalised content on an otherwise shared page | Server island |
| Slow data (recommendations, reviews) that shouldn't delay the page | Server island |
| A widget that responds to clicks and typing | Client island (client:*) |
Browser-only data like screen size or localStorage | Client island, often client:only |
| Whole page differs per user (account dashboard) | On-demand rendered page |
| Content that's the same for everyone and rarely changes | Static HTML |
Server islands and client islands combine well. A server island can contain a hydrated framework component, so a personalised "Your cart" panel can be rendered on the server with the user's items and still have interactive buttons.
Good candidates for server islands:
- Header user menu: avatar, name and sign-out link, with a generic avatar as fallback.
- Cart summary: item count read from a session cookie.
- Personalised prices or offers: discounts based on the user's account.
- Stock and delivery estimates: data from an inventory service that's too slow to wait for.
- Reviews and comments: content that changes often on a page that otherwise doesn't.
Avoid them for content that matters for SEO or that's needed for the page to make sense, because it arrives after the page and through JavaScript.
FAQ: Astro Server Islands
Yes, a small inline script that fetches the island and swaps in its HTML. Without JavaScript, visitors see the fallback content instead.
The pages can be static, but the islands must be rendered on demand, so you need an adapter and a host that can run server code. A purely static host can't serve server islands.
The server:defer directive is applied to Astro components. Inside that Astro component you can render framework components, including hydrated ones with a client directive.
Yes. It runs in its own request with the visitor's cookies, so Astro.cookies, request headers and sessions work as they do on any on-demand page.
The page was probably built with a different encryption key than the server handling the island request, which happens with rolling deploys or cached pages. Generate a key with astro create-key and set it as ASTRO_KEY for every build.
They can if the fallback is a different size from the final content. Give the fallback roughly the same dimensions, for example a fixed-size placeholder avatar, to keep the layout stable.
Conclusion
Server islands let you split a page by how often its content changes. The bulk of the page is rendered once and served fast, while components marked with server:defer are rendered on the server in their own requests, with full access to cookies, headers and data, and swapped in over fallback content. Each island loads independently, can set its own cache headers, and keeps your main page free of personal data.
Use them for personalised or slow sections of otherwise shared pages, keep their props small so requests stay cacheable, read the page URL through props or the Referer header, and set ASTRO_KEY if your deployment can serve pages and islands from different builds. For interactivity, reach for client islands instead, and combine the two when a personalised section also needs to respond to the user.


