You have a React SPA or Vite app running on Cloudflare Pages. You want to move it to Workers — maybe to use Durable Objects, gradual deployments, Tail Workers for logging, or to consolidate multiple projects into one Workers setup.
The configuration changes are not dramatic, but doing them in the wrong order will break production. A direct swap — create a Worker, point your domain at it, delete Pages — produces downtime. The safe path is to build and validate the Worker on a workers.dev subdomain before touching anything production traffic sees.
This guide covers the full sequence: wrangler configuration, SPA routing, Pages Functions migration, environment variables, the domain switchover, and rollback.
When moving from Pages to Workers makes sense
Pages is a simpler deployment target: connect a repository, configure a build command, done. Workers adds configuration overhead. The switch is worthwhile when you need:
- Durable Objects or other Workers bindings that Pages cannot access natively
- Gradual deployments to roll out changes to a percentage of traffic before full release
- Tail Workers or Logpush for structured request logging beyond what Pages provides
- Routes on a non-root path — Workers supports
example.com/api/*routes; Pages serves from the root only - Remote development with
wrangler dev --remoteagainst production bindings - Source maps in production error reporting
If none of those apply, staying on Pages is fine. This migration adds complexity without benefit unless you need what Workers offers.
What actually changes between Pages and Workers
The core difference is how you describe the project:
| | Cloudflare Pages | Cloudflare Workers |
|---|---|---|
| Config file | wrangler.toml with pages_build_output_dir | wrangler.jsonc / .toml with assets.directory |
| Dev command | wrangler pages dev (port 8788) | wrangler dev (port 8787) |
| Deploy command | wrangler pages deploy | wrangler deploy |
| SPA fallback | Automatic when _redirects has /* /index.html 200 | Explicit not_found_handling: "single-page-application" |
| API routing | Pages Functions in functions/ directory | Worker entry point with main field |
| Preview URLs | Automatic per branch | Configurable; requires Workers Builds |
| Custom domains outside Cloudflare | Supported | Not supported — domain must be on Cloudflare DNS |
That last row is the most important blocker to identify before starting.
Check your custom domain setup first
Workers custom domains require the domain to be on Cloudflare-managed nameservers. Pages does not have this requirement — you can add a custom domain to Pages from any registrar using CNAME verification.
Before migrating, open your Cloudflare dashboard and check whether your domain has an orange-cloud (proxied) DNS record under a Cloudflare zone. If the domain is managed by a third-party registrar and not pointed at Cloudflare nameservers, you will need to transfer the nameservers before the domain can be assigned to a Worker.
If you cannot transfer nameservers, Workers is not a viable migration target for that domain. Pages remains your option.
Create the wrangler.jsonc configuration
Start by creating wrangler.jsonc at your project root. Do not delete your existing Pages configuration yet.
For a static SPA with no server-side logic:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "your-app-name",
"compatibility_date": "2026-08-01",
"assets": {
"directory": "./dist",
"not_found_handling": "single-page-application"
}
}
For a project with a Worker entry point (API routes or middleware):
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "your-app-name",
"compatibility_date": "2026-08-01",
"main": "./dist-worker/index.js",
"assets": {
"directory": "./dist-client",
"not_found_handling": "single-page-application"
}
}
The name field must match the Worker name you create in the Cloudflare dashboard. If the name does not match, Workers Builds will fail.
Configure static assets and SPA routing
The SPA 404 problem
On Pages, the common pattern is a _redirects file in your output directory containing:
/* /index.html 200
This tells Pages to return index.html with a 200 status for any path that does not match a static asset, which is what React Router and similar client-side routers require.
On Workers, this is not automatic. Without configuration, a direct URL like https://example.com/dashboard returns a 404 — not your app's custom error page, but Cloudflare's own error page, because the Workers runtime has no file to serve and no Worker code handling the request. This is the most common symptom developers hit immediately after their first wrangler deploy. Setting not_found_handling to "single-page-application" restores the expected behavior:
"assets": {
"directory": "./dist",
"not_found_handling": "single-page-application"
}
With this setting, any request that does not match a static file returns index.html with a 200 status.
API routes and run_worker_first
By default, Workers serves static assets before invoking Worker code. If your Worker handles API routes like /api/users, you need Worker code to run first for those paths:
"assets": {
"directory": "./dist-client",
"not_found_handling": "single-page-application",
"run_worker_first": ["/api/*"]
}
Passing an array of glob patterns to run_worker_first causes those paths to invoke your Worker before the asset layer checks for a matching file. This is more precise than the boolean true, which would route every request through your Worker code — including requests for static files.
_redirects and redirect rules
If your _redirects file contains actual redirect rules (e.g., /old-path /new-path 301) in addition to the SPA fallback line, verify those rules against the Workers static assets routing documentation before migrating. The SPA fallback pattern (/* /index.html 200) is replaced by not_found_handling as described above. For other redirect rules, test each one on your workers.dev URL during the validation step.
_headers file
If your Pages project uses a _headers file to set custom response headers, the same file works in Workers static assets. Place it in your assets directory (the one specified in assets.directory). The syntax is unchanged:
/assets/*
Cache-Control: public, max-age=31536000, immutable
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
One important difference: headers defined in _headers apply only to static asset responses. If your Worker generates a response (through run_worker_first or a Worker-only route), you must set headers in Worker code directly.
Migrate Pages Functions to a Worker entry point
If your Pages project uses Pages Functions, you have two migration paths.
Path 1: Compile the Functions directory
If your functions are in a functions/ directory (Pages Functions format), compile them to a single Worker entry point:
npx wrangler pages functions build --outdir=./dist-worker/
This produces ./dist-worker/index.js. Set main to that path in wrangler.jsonc:
{
"main": "./dist-worker/index.js",
"assets": {
"directory": "./dist-client"
}
}
Add this compilation step to your build script so it runs before wrangler deploy.
Path 2: Rewrite as a Hono or plain Workers handler
For projects where the Pages Functions are minimal (a few API endpoints), rewriting them as a standard Workers handler is often cleaner. For example:
import { Hono } from "hono";
const app = new Hono();
app.get("/api/health", (c) => c.json({ ok: true }));
export default app;
If your Worker uses KV, D1, or other bindings, pass the generated Env type: new Hono<{ Bindings: Env }>(). Run npx wrangler types to generate the type definition file.
With this approach, configure run_worker_first to cover the API paths and let everything else fall through to static assets.
Advanced Mode (_worker.js)
If you used Pages' advanced mode with a _worker.js file in your output directory, move that file outside the assets directory or list it in .assetsignore. Then set main to point to it:
{
"main": "./dist/_worker.js",
"assets": {
"directory": "./dist",
"not_found_handling": "single-page-application"
}
}
Add _worker.js to .assetsignore in your assets directory to prevent it from being uploaded as a static asset:
_worker.js
Move environment variables and bindings
Workers does not inherit environment variables from your Pages project. You need to recreate them.
Production variables: In the Cloudflare dashboard, open your Worker → Settings → Variables and Secrets. Add each variable that your Pages project defines under Settings → Environment Variables.
Preview variables: Workers Builds maintains separate environments (production and preview). Configure preview variables in the same location. Note that Workers treats build-time variables (used during the build command) and runtime variables (used by the Worker) separately. A variable visible during build is not automatically available at runtime.
KV namespaces, D1 databases, and other bindings: Add them in wrangler.jsonc and bind them in the dashboard. The binding names must match what your Worker code expects. This is the same as any Workers project; nothing specific to the migration.
Test on a workers.dev URL before touching production
Deploy the Worker to its workers.dev subdomain before assigning any custom domain. This is the core of the no-downtime approach: your Pages project continues serving production traffic while you validate the Worker.
npx wrangler deploy
This deploys to your-app-name.your-account.workers.dev. Run through these checks before proceeding:
- Load the root URL. Confirm the page renders.
- Navigate to a deep URL (e.g.,
/dashboard/settings). Confirm you get200withindex.html, not a404. - Make API requests to your API routes. Confirm they reach your Worker, not the asset layer.
- Check static asset caching headers in the response (should include
Cache-Control). - Test any auth flows or third-party integrations that depend on the domain — some will fail because the
workers.devdomain differs from your production domain. Note them; they will work correctly once the custom domain is assigned.
If anything fails here, fix and redeploy before touching the custom domain.
Switch the custom domain without downtime
With the Worker validated, the domain switch is safe. Because both Pages and Workers operate behind Cloudflare's proxy, the change is near-instantaneous when your domain is on Cloudflare DNS — there is no DNS propagation delay.
The sequence:
Step 1 — Remove the custom domain from Pages. In the Cloudflare dashboard, open your Pages project → Custom Domains. Remove the domain assignment. Pages stops serving requests for that domain immediately.
Step 2 — Assign the custom domain to the Worker. Open your Worker → Settings → Domains & Routes → Add Custom Domain. Enter the same domain. Cloudflare routes traffic to the Worker, typically within a few seconds.
Step 3 — Confirm the Worker is serving the custom domain. Load your site at the custom domain. Verify that the page renders and that auth flows using the domain now work.
Step 4 — Do not delete the Pages project yet. Keep the Pages project intact as a rollback option. Deleting it before the Worker is confirmed stable removes your fallback.
Between Step 1 and Step 2 there is a window — usually a few seconds — where the domain is unassigned. This is the only unavoidable gap. To minimize it, have the Worker dashboard tab open and ready before removing the domain from Pages. For most production situations this window is acceptable; for zero-tolerance setups, schedule the switch during off-peak hours.
Update your CI/CD pipeline
Pages uses the wrangler pages deploy command in CI. Workers uses wrangler deploy.
Workers Builds (recommended): Connect your repository to Workers Builds in the Cloudflare dashboard (Worker → Settings → Builds). Workers Builds runs your build command and calls wrangler deploy on successful builds. Configure it with:
- Build command (e.g.,
npm run build) - Build output directory (not strictly needed;
wrangler deployreads fromwrangler.jsonc) - The Worker name must match
nameinwrangler.jsonc
Once Workers Builds is active, disable automatic deployments on your Pages project to prevent both deploying on push.
GitHub Actions: If you use a GitHub Actions workflow, replace wrangler pages deploy with wrangler deploy and remove the --project-name flag:
- name: Deploy
run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
For preview environments, Workers Builds generates preview URLs for non-production branches when enabled in the dashboard (Worker → Settings → Builds → Enable branch deployments).
Rollback procedure
If the Worker has a problem after the domain switch:
- Open the Worker → Settings → Domains & Routes. Remove the custom domain assignment.
- Open the Pages project → Custom Domains. Re-add the custom domain.
Pages resumes serving traffic. The Pages project you kept intact is the rollback target. This is why you do not delete Pages immediately.
For a faster rollback on the Worker itself (without involving the domain): Workers Builds maintains a version history. You can promote a previous version from the dashboard or using wrangler rollback.
Migration checklist
Copy and work through this before deleting the Pages project.
Configuration
- [ ]
wrangler.jsonccreated with correctname,compatibility_date,assets.directory - [ ]
not_found_handling: "single-page-application"set (if SPA) - [ ]
run_worker_firstconfigured for API paths (if Worker entry point exists) - [ ]
_headersfile placed in assets directory and verified - [ ]
.assetsignorecreated if needed (e.g., to exclude_worker.js)
Functions migration
- [ ] Pages Functions compiled or rewritten as Worker entry point
- [ ]
mainfield inwrangler.jsoncpoints to correct output file - [ ] API routes respond correctly on
workers.dev
Environment
- [ ] All production environment variables recreated in Worker settings
- [ ] All preview environment variables recreated
- [ ] KV, D1, R2, and other bindings added to
wrangler.jsoncand dashboard
Pre-switchover validation
- [ ] Root URL loads correctly on
workers.dev - [ ] Deep SPA routes return
200, not404, onworkers.dev - [ ] API routes respond correctly
- [ ] Static assets have correct cache headers
Custom domain
- [ ] Domain is on Cloudflare-managed nameservers (required for Workers)
- [ ] Custom domain removed from Pages
- [ ] Custom domain assigned to Worker
- [ ] Site verified at custom domain after assignment
CI/CD
- [ ] Workers Builds connected to repository, or GitHub Actions workflow updated
- [ ] Automatic Pages deployments disabled
- [ ] First post-migration build passes
Cleanup (after 1–2 weeks of stable operation)
- [ ] Pages project deleted (or archived)
- [ ] Old wrangler.toml or Pages-specific config removed from repository
If you need to keep triggering Cloudflare Pages rebuilds from another repository rather than migrating to Workers, see How to Redeploy Cloudflare Pages When Another Repository Changes.