Cloudflare Adapter
Astro renders to static HTML by default, but when you need on-request rendering — auth, personalization, API routes, form handling — you need an adapter that maps Astro’s server output onto your host’s runtime. The @astrojs/cloudflare adapter targets Cloudflare’s edge, compiling your SSR routes into a Cloudflare Worker that runs in V8 isolates within milliseconds of your users worldwide. It gives you direct access to Cloudflare’s bindings — KV, D1, R2, Durable Objects — while preserving Astro’s zero-JS-by-default islands so you only ship dynamic behavior where you opt in.
Installing the adapter
The fastest path is astro add, which installs the package, updates astro.config.mjs, and sets the output mode for you.
npx astro add cloudflare
To do it manually, install the package and register the adapter yourself:
npm install @astrojs/cloudflare
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server',
adapter: cloudflare(),
});
The Cloudflare runtime is the Workers runtime (workerd), not Node.js. It exposes Web-standard APIs —
fetch,Request,Response,crypto.subtle, streams — but not Node built-ins likefs. SetplatformProxy.enabled(shown below) soastro devemulates those bindings locally.
Output modes
Astro distinguishes how each page is rendered. Pick the mode that matches how much of your site is dynamic.
output value | Behavior | Use when |
|---|---|---|
static | Everything prerendered to HTML at build time | Mostly content; no per-request logic |
server | On-demand rendering by default; opt out per page | Dashboards, auth, APIs |
With output: 'server' you can still prerender individual pages, keeping them as static assets served straight from Cloudflare’s CDN:
---
// src/pages/about.astro
export const prerender = true;
---
<html>
<body><h1>About us</h1></body>
</html>
Under static output you opt a single route into on-demand rendering with export const prerender = false.
Accessing the runtime and bindings
Cloudflare injects environment variables, secrets, and resource bindings (KV, D1, R2) through the runtime context rather than process.env. The adapter surfaces them on Astro.locals.runtime. Configure local emulation so the same code works in astro dev:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server',
adapter: cloudflare({
platformProxy: { enabled: true },
}),
});
Declare your bindings in wrangler.toml so they exist both locally and in production:
# wrangler.toml
name = "astro-app"
compatibility_date = "2025-05-01"
compatibility_flags = ["nodejs_compat"]
[[kv_namespaces]]
binding = "SESSIONS"
id = "f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6"
Then read a binding inside a server endpoint:
// src/pages/api/visits.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ locals }) => {
const { SESSIONS } = locals.runtime.env;
const count = Number((await SESSIONS.get('visits')) ?? 0) + 1;
await SESSIONS.put('visits', String(count));
return new Response(JSON.stringify({ visits: count }), {
headers: { 'Content-Type': 'application/json' },
});
};
Output:
$ curl "https://astro-app.pages.dev/api/visits"
{"visits":42}
For type safety, generate types from your config with Wrangler and reference them so locals.runtime.env is fully typed:
npx wrangler types
Per-route configuration
Because every dynamic route runs on the same Worker, you control delivery with prerender per page rather than choosing a runtime per route. Use route-level prerendering to push as much as possible to the static CDN tier:
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
export const prerender = true;
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
Gotcha: code importing Node core modules will fail unless you set
compatibility_flags = ["nodejs_compat"]and a recentcompatibility_date. Even then, prefer Web-standard APIs and Cloudflare bindings — they are faster to start and fully supported on the edge.
Deploying
The adapter builds output for both Cloudflare Pages and standalone Workers. With Pages, connect the Git repository in the dashboard (it auto-detects Astro), or deploy from the CLI with Wrangler:
npm run build
npx wrangler pages deploy ./dist
Output:
✨ Compiled Worker successfully
🌎 Uploading... (12/12)
✨ Deployment complete!
https://astro-app.pages.dev
Store secrets with npx wrangler pages secret put MY_SECRET rather than committing them, and they appear on locals.runtime.env alongside your bindings.
Best practices
- Enable
platformProxy: { enabled: true }soastro devemulates KV, D1, R2, and secrets locally instead of failing on missing bindings. - Read environment values from
Astro.locals.runtime.env, neverprocess.env— the Workers runtime does not populateprocess.envby default. - Prerender every route you can with
export const prerender = trueso only truly dynamic pages run the Worker. - Declare bindings in
wrangler.tomland runwrangler typesto keeplocals.runtime.envstrongly typed. - Stick to Web-standard APIs and Cloudflare bindings; reach for
nodejs_compatonly when a dependency genuinely needs a Node built-in. - Manage secrets with
wrangler pages secret putand reference them through the runtime, keeping them out of version control.