Cloudflare Pages vs Workers: Which to Deploy On

Cloudflare’s own Pages documentation now answers this for you: “Workers supports most Pages use cases and offers a broader feature set. It is Cloudflare’s primary platform for building applications. Start new projects with Workers.” Pages still runs, and existing projects keep working untouched, but new deployments belong on Workers with Static Assets.

That advice landed quietly, and a lot of teams are still reaching for wrangler pages deploy out of habit. Here is what actually changed as of August 2026, what it costs you to move, and the cases where staying put is the right call. Cloudflare reshapes this surface quickly, so treat every row of the compatibility matrix below as something to re-check before you plan around it.

What does Workers Static Assets do that Workers could not before?

It serves files. That sounds trivial, and it is the whole point: until Static Assets shipped, a Worker was a script, and anything resembling a website needed Pages or the now-deprecated Workers Sites. Now you point Wrangler at a build directory and Cloudflare uploads your HTML, CSS and images alongside the script, caches them at the edge and serves them without invoking your code.

For a pure static build, an Astro dist/, a Vite output, any SSG, you do not need a Worker script at all:

{
  "name": "whoooop-site",
  "compatibility_date": "2026-08-24",
  "assets": {
    "directory": "./dist/",
    "not_found_handling": "404-page",
    "html_handling": "auto-trailing-slash"
  }
}

npx wrangler deploy and you have a site. html_handling takes four values (auto-trailing-slash, the default, plus force-trailing-slash, drop-trailing-slash and none), which matters more than it sounds if you have canonical URLs and redirects you would rather not break. not_found_handling is where you choose between a 404-page and single-page-application fallback.

Add a script and you get the full-stack shape:

interface Env {
  ASSETS: Fetcher;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname.startsWith('/api/')) {
      return Response.json({ ok: true, path: url.pathname });
    }

    return env.ASSETS.fetch(request);
  },
} satisfies ExportedHandler<Env>;
{
  "name": "whoooop-app",
  "main": "./worker/index.ts",
  "compatibility_date": "2026-08-24",
  "assets": {
    "directory": "./dist/client/",
    "binding": "ASSETS",
    "run_worker_first": ["/api/*"]
  }
}

By default a request that matches a file is served straight from the asset store and your script never runs. run_worker_first inverts that for the routes you name, and it takes negations too (["/api/*", "!/api/docs/*"]), which is the closest thing to Pages middleware. Those invocations bill as normal Worker requests.

Cloudflare Pages vs Workers: which should you deploy on?

New project, no legacy: Workers. There is no argument left on that side.

Existing Pages project that builds, deploys and serves without complaint: leave it alone until you want something Pages cannot give you. Cloudflare has not announced an end date, and the compatibility matrix still shows Pages winning a couple of rows.

The clearest reasons to move an existing project are on the operational side rather than the serving side. Cron Triggers, Workers Logs, Logpush, Tail Workers, gradual deployments and remote development are all marked as Workers-only in Cloudflare’s compatibility matrix. So is the Cloudflare Vite plugin. If you have been running a second standalone Worker next to your Pages project just to hold a scheduled job or a Durable Object, that split disappears.

Cost is close to a wash, with one asymmetry worth knowing. Requests to static assets are free and unlimited on Workers, and there is no charge for storing them. Pages Functions invocations are billed at the same rate as Workers invocations, so dynamic traffic costs the same either way. The saving only shows up on file-serving traffic.

Watch the platform limits before you commit: 20,000 files per Worker version on the free plan, 100,000 on paid, and 25 MiB per individual file. A documentation site with thousands of generated pages and a fat image directory can get closer to that ceiling than you would expect.

What breaks when you migrate off Pages?

File-based routing is the big one. The functions/ folder does not exist on Workers. You can compile it once with

npx wrangler pages functions build --outdir=./dist/worker/

and point main at the output, and Cloudflare says the command stays available, but their own guidance is to move to a framework that does routing for you rather than treating the compiled bundle as a permanent home.

_headers and _redirects still work, and the syntax is unchanged, so long as the files sit in your assets directory. The limits carry over: up to 100 header rules, 2,000 characters per line. The trap is a sentence people skim past in the headers documentation: custom headers from _headers are not applied to responses generated by your Worker code. If your CSP and HSTS headers live in that file and you have SSR routes, those routes ship without them. Set them in the script instead, or in a Transform Rule.

Early Hints is partially supported and needs a zone setting plus explicit Link headers. Pages Plugins have no direct equivalent, so anything built on one needs rewriting as Worker code or as a framework plugin.

How do you migrate an existing Pages project?

Six steps, in the order the migration guide lays them out.

Swap your framework adapter for the Workers variant. Create a Wrangler config with name and compatibility_date. Replace pages_build_output_dir with assets.directory. Pick your not_found_handling explicitly rather than inheriting whatever Pages did. Compile any functions/ directory with the command above. Then replace wrangler pages dev and wrangler pages deploy with plain wrangler dev and wrangler deploy.

Git integration is the part people assume they will lose. Workers Builds covers it: connect a GitHub or GitLab repository and pushes build and deploy automatically, with preview URLs generated per version. One gotcha will cost you a failed build the first time. The Worker name in the dashboard has to match the name field in the Wrangler config at the specified root directory, or the build fails outright.

Budget an afternoon for a static site and a day or two for anything with Functions and bindings. The bindings themselves (KV, D1, R2, AI, Queues) move across as configuration, not code.

Would we move a working Pages project today?

For a static marketing site with no Functions, no. It gains you free asset requests you were probably not paying much for anyway, and costs you a deployment surface you have already tested. Do it opportunistically, the next time you touch the build.

For anything with a functions/ folder, a scheduled job bolted on beside it, or a real need to read production logs, move it now while the project is small enough to hold in your head. That decision gets more expensive every quarter you leave it. Our notes on Vercel’s trade-offs cover the same question from the other side of the market, and if you would rather hand the whole thing over, platform migrations are part of what we do.

Need this built properly?

Whoooop Ltd has spent 15+ years building and maintaining web applications in TypeScript, React, Node.js and serverless — the same ground this post covers.

Get in touch