Compute · Pages

Pages or Workers? Choosing the right compute model

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.

Convergence, not replacement. Cloudflare's static-assets-for-Workers documentation is explicit that both platforms remain supported and cost the same for equivalent workloads: static asset requests are free on both, and function/Worker invocations are billed at the same rate. The difference is feature surface, not price or support status.

What each one actually is

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.

How they actually differ now

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:

What Cloudflare actually recommends

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.

Worked example: a feedback form with an API

Same small app — a static HTML form plus a /api/feedback endpoint that validates input and writes to storage — structured both ways.

As a Pages project

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.

As a plain Worker with an assets binding

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.

Use cases matrix

ScenarioBetter fitWhy
Marketing site or docs, content team pushes via git, no backend logic beyond a contact formPagesGit dashboard, automatic previews, zero Wrangler config to maintain
API-first app that also needs Cron Triggers, Durable Objects, or QueuesWorkerThose bindings aren't available (or are indirect) on Pages Functions
Full-stack app (frontend + API) starting from scratch todayWorker with assets bindingSuperset 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 itPagesNo compelling reason to migrate a working setup — Pages remains supported
Pitfall: assuming one platform strictly deprecates the other. It's easy to read "Workers has a broader feature set" and conclude Pages is being sunset, or conversely to assume Pages must remain the default because it's older and specialized for static sites. Neither is Cloudflare's actual position. Their docs confirm existing Pages projects keep working, pricing is equivalent for equivalent workloads, and migration is offered as a straightforward option if you need it — not a mandate. Don't migrate a working Pages app just because a lesson (or a blog post) implies you should; migrate when you actually hit a feature gap (Cron Triggers, direct Durable Objects, fine-grained asset routing, etc.).
Primary source

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.

Your team's Pages site works fine today but you're wondering if you should migrate to a plain Worker "to be safe." According to Cloudflare's own docs, what's the correct read of the situation?
Without scrolling up: name one capability Pages Functions cannot use that a plain Worker can, and name the Pages-side strength that a plain Worker only gets via an opt-in feature (not by default).
Reveal

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.

Anything above unclear — the assets-binding routing options, when Workers Builds vs Pages' git integration makes more sense for your team, or how D1 bindings differ between the two setups? Ask your AI teacher before moving on.
← Previous: Ship a full-stack frontend with Pages Next: Resize, optimize, and serve images at the edge →