Deploying Astro to Cloudflare Workers Static Assets

Cloudflare's hosting for static sites has converged on Workers: a Worker project can serve a directory of static assets from the edge for free, run code only for the routes that need it, and use the full set of Worker features — versions and gradual rollouts, cron triggers, queues, Durable Objects — alongside. For a static Astro site, that means a deploy target with Pages-like simplicity and a clear path to adding server-rendered routes or APIs later without changing platforms.

This guide deploys a 410-page static Astro site to Workers static assets: the wrangler configuration, matching Astro's URL settings, headers and redirects, 404 handling, preview versions and gradual rollouts, and adding a single server route with the Cloudflare adapter. It is part of Cloudflare Pages Edge Caching Setup, and the migration from an existing Pages project is covered in Migrating from Cloudflare Pages to Workers Static Assets.

Prerequisites

  • Astro 4.16 or 5.x with output: 'static' (the default).
  • Wrangler 3.90+ and a Cloudflare account; a custom domain on Cloudflare DNS.
  • CI credentials: an API token with Workers edit permission, stored as a secret.

Step 1: Align Astro's URL Settings

Decide on URL shape first, because Astro and the asset server must agree. For directory-style URLs with trailing slashes:

// astro.config.mjs
export default defineConfig({
  site: 'https://docs.example.com',
  trailingSlash: 'always',
  build: { format: 'directory' }        // about/index.html
});

This matches Workers' default html_handling = "auto-trailing-slash", which serves about/index.html at /about/ and redirects /about to /about/ with a 307. If Astro used build.format: 'file' (about.html), the same setting serves it at /about and redirects /about/ to /about. Consistency with canonical URLs and internal links matters more than which one you pick; see Canonical URLs and Duplicate Content on Static Sites.

Step 2: The Wrangler Configuration

A fully static site needs no Worker script at all:

# wrangler.toml
name = "docs-site"
compatibility_date = "2026-09-01"

[assets]
directory = "./dist"
html_handling = "auto-trailing-slash"
not_found_handling = "404-page"

[[routes]]
pattern = "docs.example.com"
custom_domain = true

not_found_handling = "404-page" serves the nearest 404.html walking up from the requested path, with a 404 status — so /de/missing/ gets dist/de/404.html if one exists, which suits multilingual sites. custom_domain = true creates the DNS record and certificate automatically.

Deploy with npx wrangler deploy. Requests for static files are served from Cloudflare's edge without invoking code and are not billed as Worker requests.

Request handling layers for Astro on Workers static assets A request reaches the Cloudflare edge. Redirect rules from the _redirects file are applied first. Then the asset server looks for a matching file using html_handling for trailing slashes. If found it is served with headers from the _headers file. If no file matches and a Worker script exists with run_worker_first or as fallback, the script handles it; otherwise not_found_handling serves the nearest 404.html. How a request is answered _redirects 1,400 rules, static evaluated first Asset lookup auto-trailing-slash about/ → about/index.html _headers cache + security headers applied to asset responses Worker script (optional) server routes only runs only when no asset matches 404-page nearest 404.html status 404, not 200
For this site, no request ever reached a Worker script until the one server route was added.

Step 3: Headers and Redirects

Workers static assets read _headers and _redirects files from the assets directory, in the same format as Pages. Put them in Astro's public/ folder so they are copied to dist/:

# public/_headers
/_astro/*
  Cache-Control: public, max-age=31536000, immutable
/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Strict-Transport-Security: max-age=31536000; includeSubDomains
# public/_redirects
/docs/installation.html   /guide/install/   301
/blog/:year/:slug/        /articles/:slug/  301

HTML gets no explicit Cache-Control here; Workers static assets serve HTML with revalidation by default and handle cache invalidation on deploy themselves, so no purge step is needed. Redirect limits and splats behave as on Pages; generating _redirects from a data file keeps it testable, as in Testing Redirects and Headers Before Deploy.

Step 4: Preview Versions and Gradual Rollouts

Workers separates uploading a version from deploying it. In CI, pull requests upload a version and get a preview URL; merges to main deploy:

# pull request job
- run: npm ci && npm run build
- run: npx wrangler versions upload --tag "pr-${{ github.event.number }}" --message "${{ github.event.pull_request.title }}"
  env: { CLOUDFLARE_API_TOKEN: '${{ secrets.CF_WORKERS_TOKEN }}' }
# main job
- run: npx wrangler deploy

versions upload prints a preview URL for that version (https://<version-prefix>-docs-site.<account>.workers.dev), which the workflow comments on the pull request. For riskier changes, wrangler versions deploy can split traffic — 10% to the new version and 90% to the current one — and then move to 100% after checks pass, the approach described in Canary Releases for Static Sites. Rolling back is wrangler rollback to the previous version, which took under 10 seconds to propagate in testing.

Version workflow for a static Astro site on Workers A pull request job builds the site and uploads a new version, which receives a preview URL but no production traffic. A reviewer checks the preview. On merge, the main job deploys the new version to 100 percent of traffic, or to a percentage first. If something goes wrong, rollback returns traffic to the previous version. Upload, preview, deploy PR job build + upload Workers versions stored per upload Production traffic docs.example.com versions upload (preview URL) review the preview deploy on merge: 10% then 100% rollback if needed (< 10 s)
Uploading never changes production; only deploy and rollback move traffic.

Step 5: Adding One Server Route Later

Months after launch, the site needed a feedback endpoint that writes to a D1 database. With the @astrojs/cloudflare adapter, Astro can keep every page prerendered and add a single on-demand route:

// astro.config.mjs
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({ output: 'static', adapter: cloudflare() });
// src/pages/api/feedback.ts
export const prerender = false;
export async function POST({ request, locals }) {
  const { page, helpful } = await request.json();
  await locals.runtime.env.DB.prepare('INSERT INTO feedback (page, helpful) VALUES (?, ?)').bind(page, helpful ? 1 : 0).run();
  return new Response(null, { status: 204 });
}

The adapter generates a Worker script and configures run_worker_first so only /api/* invokes it; every page remains a static asset. The wrangler config gains a main entry and a [[d1_databases]] binding. Monthly Worker invocations after the change: about 38,000 — the feedback submissions — while the site served 2.4 million static page views.

Observability and Limits

Static asset requests do not run code, so they do not appear in Worker logs; traffic, cache and status analytics for them are in the zone's analytics and the Workers dashboard's asset metrics. Enable [observability] in the wrangler config once a Worker script exists, so server routes get logs and traces without extra setup. Two limits are worth knowing before a large site moves: individual asset files are limited to 25 MiB, which rules out large video files (host those elsewhere), and the number of files per version is capped in the tens of thousands on the free plan — comfortably above this site's 1,900 files, but relevant for sites that generate an image variant per width and format for thousands of photos. Checking the file count in CI against the plan's limit turned a potential deploy failure into an early warning; the build-output counting follows the same approach as Deploying Only Changed Files.

Keep the wrangler configuration in the repository next to the Astro config, and review changes to both together; most deployment surprises on this site came from one being changed without the other.

Measured Impact

Compared with the site's previous deployment on Cloudflare Pages:

MeasureCloudflare PagesWorkers static assets
Deploy time (upload 1,900 files, 740 changed)38 s29 s
TTFB p75 for HTML, field112 ms104 ms
Rollback timeredeploy of previous build, ~40 swrangler rollback, < 10 s
Gradual rolloutnot availablepercentage split per version
Worker invocations for static pages00
Monthly cost00 (static) + feedback route within free tier
Rollback time on Pages and on Workers static assets Vertical bars. Rolling back on Cloudflare Pages by redeploying the previous build took about 40 seconds until the old version served globally. Rolling back on Workers with wrangler rollback took under 10 seconds. Time to roll back a bad deploy 40 s Pages redeploy 9 s Workers rollback
Measured from command to the old version being served by curl from three regions.

Pitfalls & Rollback

  • Mismatched trailing-slash settings. Astro's trailingSlash and build.format must match html_handling, or every internal link redirects.
  • Using single-page-application 404 handling for a static site. Missing pages return the home page with a 200, which search engines treat as soft 404s.
  • Placing _headers outside public/. It must end up in the assets directory.
  • Forgetting prerender = false on server routes. Otherwise the route is built as a static file and POST requests fail.
  • Rollback: wrangler rollback restores the previous version; moving back to Pages means pointing the custom domain at the Pages project again.

Conclusion

Workers static assets serve a static Astro site as simply as Pages does — one config file, _headers and _redirects in public/, no code for static requests — and add versioned previews, gradual rollouts, fast rollbacks and a path to server routes without changing platforms. Align Astro's URL settings with the asset server's, keep 404s honest, and add on-demand routes only where they are needed.

FAQ

Why deploy to Workers static assets instead of Cloudflare Pages?

Workers with static assets is Cloudflare's current platform for both static and dynamic sites, with the same free static asset serving as Pages plus full Worker features such as cron triggers, Durable Objects, more bindings and gradual deployments. New projects increasingly start there, and Pages projects can migrate.

Does a fully static Astro site need a Worker script?

No. With only an assets directory configured in wrangler, Cloudflare serves the files without running any code, and requests are not billed as Worker invocations. A script is only needed for server routes, middleware or custom logic.

How are trailing slashes and 404s handled?

The assets configuration has html_handling, which defaults to auto-trailing-slash and serves about/index.html at /about/, and not_found_handling, which can serve the nearest 404.html or act as a single-page app fallback. Set them to match Astro's trailingSlash and build.format settings.

How do preview deployments work?

wrangler versions upload creates a new version with a preview URL without routing production traffic to it. Promoting it with wrangler versions deploy, optionally as a gradual percentage rollout, makes it live. Git-connected Workers builds can create previews per branch automatically.

Filed underHow-tosAstroCloudflare