After this lesson you'll be able to pick between Cloudflare Pages and a plain Worker for a new project, justify the choice from Cloudflare's own guidance, and restructure the same app either way.
Cloudflare Pages and Cloudflare Workers used to be two separate products: Pages for git-connected static/full-stack sites, Workers for programmable edge functions. That line has mostly disappeared. Workers can now serve static assets directly (an assets binding, no separate product needed), which means a plain Worker can do almost everything Pages does. Pages hasn't gone away and isn't deprecated — but for a brand-new project, Cloudflare's own docs now point most people toward Workers first. This lesson is about knowing why, and knowing the cases where Pages still wins.
Pages is a deploy target built around git: connect a GitHub/GitLab repo, Cloudflare builds it on every push, and you get a production deployment plus a unique preview URL for every branch and pull request automatically. Dynamic behavior comes from Pages Functions — files under a functions/ directory that map to routes, running on the Workers runtime underneath.
A plain Worker is a single script (or module) you deploy with Wrangler. It has full programmatic control over routing, and since the introduction of the assets binding it can also serve a directory of static files — configured in wrangler.jsonc — with the option to run your own code first, fall back to your own code when no asset matches, or serve assets straight from cache with no Worker invocation at all.
Both platforms run on the same underlying Workers runtime and both can serve a static frontend plus a dynamic API. The differences that remain are concrete, not philosophical:
run_worker_first and not_found_handling options give you fine-grained control over exactly when your code runs versus when the platform serves an asset directly. Pages Functions' routing model (file-based, under functions/) is simpler but less flexible — e.g. Pages cannot serve assets scoped to specific paths the way a Worker's assets config can.wrangler deploy from your machine or CI — git integration via Workers Builds is opt-in, not the default mental model.There is no blanket "Pages is deprecated, migrate now" statement in Cloudflare's docs — treat any claim to the contrary as a misreading. What the docs do say, concretely:
In practice: pick a plain Worker by default for a new project unless git-native preview-per-PR with zero extra config is the thing you value most, or your team's workflow is already built around Pages' git dashboard.
Same small app — a static HTML form plus a /api/feedback endpoint that validates input and writes to storage — structured both ways.
my-app/
├── public/ # static assets, deployed as-is
│ ├── index.html
│ └── style.css
└── functions/
└── api/
└── feedback.ts # maps to POST/GET /api/feedback
// functions/api/feedback.ts
interface Env {
FEEDBACK_DB: D1Database;
}
export const onRequestPost: PagesFunction<Env> = async (context) => {
const { message, email } = await context.request.json();
if (!message || typeof message !== "string") {
return Response.json({ error: "message is required" }, { status: 400 });
}
await context.env.FEEDBACK_DB
.prepare("INSERT INTO feedback (message, email) VALUES (?, ?)")
.bind(message, email ?? null)
.run();
return Response.json({ ok: true });
};
Deploy by connecting the repo in the dashboard (or wrangler pages deploy public); every push to a branch gets its own preview URL with no extra config.
my-app/
├── public/ # static assets, referenced by wrangler config
│ ├── index.html
│ └── style.css
├── src/
│ └── index.ts # the Worker script — all routing is explicit
└── wrangler.jsonc
// wrangler.jsonc
{
"name": "feedback-app",
"main": "src/index.ts",
"compatibility_date": "2026-01-01",
"assets": {
"directory": "./public",
"binding": "ASSETS",
"not_found_handling": "single-page-application"
},
"d1_databases": [
{ "binding": "FEEDBACK_DB", "database_name": "feedback", "database_id": "<uuid>" }
]
}
// src/index.ts
interface Env {
ASSETS: Fetcher;
FEEDBACK_DB: D1Database;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/api/feedback" && request.method === "POST") {
const { message, email } = await request.json();
if (!message || typeof message !== "string") {
return Response.json({ error: "message is required" }, { status: 400 });
}
await env.FEEDBACK_DB
.prepare("INSERT INTO feedback (message, email) VALUES (?, ?)")
.bind(message, email ?? null)
.run();
return Response.json({ ok: true });
}
// Not an API route — fall back to serving the static asset.
return env.ASSETS.fetch(request);
},
};
The Worker version makes routing explicit in code (you can see exactly when the API branch runs versus when it falls through to assets), and the same script could add a Durable Object, a Cron Trigger, or a Queue consumer without switching platforms. Deploy with wrangler deploy, or connect the same repo via Workers Builds for the equivalent git-push-to-deploy and per-branch preview behavior.
| Scenario | Better fit | Why |
|---|---|---|
| Marketing site or docs, content team pushes via git, no backend logic beyond a contact form | Pages | Git dashboard, automatic previews, zero Wrangler config to maintain |
| API-first app that also needs Cron Triggers, Durable Objects, or Queues | Worker | Those bindings aren't available (or are indirect) on Pages Functions |
| Full-stack app (frontend + API) starting from scratch today | Worker with assets binding | Superset of features from day one; no future migration if requirements grow |
| Team already has a Pages-based CI workflow and the app's needs haven't outgrown it | Pages | No compelling reason to migrate a working setup — Pages remains supported |
Migrate from Pages to Workers is Cloudflare's clearest side-by-side statement of the current feature gap and migration guidance; pair it with the Static Assets on Workers docs for the assets-binding mechanics used in the worked example above.
Pages Functions cannot use Cron Triggers, Logpush, Tail Workers, Queues consumers, Rate Limiting, or Email Workers bindings, and only get indirect Durable Objects access — any of these would push you toward a plain Worker.
Git-push-to-deploy with automatic per-branch preview URLs is the Pages default; a plain Worker gets the equivalent behavior only by opting into Workers Builds, not out of the box.