Quick verdict
- Use Workers when you need edge logic: API backend, proxy, auth, middleware, job handler — no static file serving needed
- Use Pages when you have a frontend: Next.js, Astro, SvelteKit, React SPA — Pages handles both hosting and server-side code (Pages Functions run on the Workers runtime under the hood)
- Not an either/or choice: many projects use Pages for the frontend and a separate Worker for the backend API, both binding to the same KV/D1/R2
Quick comparison
| Workers | Pages | |
|---|---|---|
| Primary purpose | Serverless function at the edge | Static hosting + Functions |
| How to deploy | wrangler deploy |
Git push (or wrangler pages deploy) |
| Serve static files | No (needs R2 or KV) | Yes — automatically from build output |
| Server-side logic | The whole thing is logic | Pages Functions (Workers runtime underneath) |
| Framework support | None built-in | Next.js, Astro, SvelteKit, Remix... |
| Bind KV/D1/R2/Queue | Yes | Yes (inside Pages Functions) |
| Cron triggers | Yes ([triggers] in wrangler.toml) |
No |
| Free tier requests | 100,000 req/day | Unlimited (static files, no cap) |
| Free tier builds | Not applicable | 500 builds/month |
| Custom domain | Yes | Yes |
| Preview deployments | No | Yes (each branch/PR gets a preview URL) |
What Workers is and what it does
Workers is Cloudflare's serverless function platform, running at the edge — your code runs in a Cloudflare datacenter near the user, not on a fixed server.
Each Worker receives an HTTP request, processes it, and returns a response. No server, no container, no meaningful cold start.
A basic Worker looks like this
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/api/ping") {
return Response.json({ ok: true, ts: Date.now() });
}
return new Response("Not found", { status: 404 });
},
};
Deploy with:
wrangler deploy
What Workers is good for
Pure API backend
When a mobile app or SPA needs a lightweight API without standing up a dedicated server:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method === "POST" && new URL(request.url).pathname === "/api/contact") {
const body = await request.json<{ email: string; message: string }>();
await env.DB.prepare("INSERT INTO contacts (email, message) VALUES (?, ?)")
.bind(body.email, body.message)
.run();
return Response.json({ ok: true });
}
return new Response("Not found", { status: 404 });
},
};
Proxy and middleware
Intercept requests before they reach the origin: validate tokens, rate limit, rewrite URLs, inject headers:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const token = request.headers.get("Authorization");
if (!token || !(await verify(token, env.JWT_SECRET))) {
return new Response("Unauthorized", { status: 401 });
}
return fetch(request); // forward to origin
},
};
Edge cron jobs
Workers can run on a schedule without any incoming request:
# wrangler.toml
[triggers]
crons = ["0 * * * *"] # every hour
export default {
async scheduled(event: ScheduledEvent, env: Env): Promise<void> {
await syncDataFromExternalApi(env);
},
};
Pages Functions don't support cron triggers.
Queue consumer
Process jobs from Cloudflare Queues:
export default {
async queue(batch: MessageBatch<Job>, env: Env): Promise<void> {
for (const msg of batch.messages) {
await processJob(msg.body, env);
msg.ack();
}
},
};
What Pages is and what it does
Pages is Cloudflare's static hosting platform. Connect a GitHub/GitLab repo, and every push triggers an automatic build and deploy to a global CDN.
The key detail: Pages isn't just for serving HTML/CSS/JS — it has Pages Functions, which let you write server-side logic directly inside the project, with the same ability to bind KV/D1/R2/Queue as Workers.
Project structure with Pages Functions
my-app/
├── src/ # frontend code (Next.js, Astro, ...)
├── functions/ # Pages Functions
│ ├── api/
│ │ ├── posts.ts # → /api/posts
│ │ └── contact.ts # → /api/contact
│ └── _middleware.ts # runs before every request
└── package.json
Pages Functions route automatically by filename — no extra config needed.
What Pages is good for
Full-stack app with a framework
Pages supports Next.js, Astro, SvelteKit, Remix, Nuxt, and more via adapters. Build once, Pages handles deploy and CDN:
# Next.js on Pages
npm install @cloudflare/next-on-pages
# deploy
wrangler pages deploy .vercel/output/static
Static site / blog
Blogs built with Astro, Hugo, Eleventy — Pages serves files fast from the edge, no server needed:
# Astro: build and deploy
npm run build
wrangler pages deploy dist
App with frontend + lightweight server
A SPA that needs a few endpoints — write Pages Functions instead of standing up a separate Worker:
// functions/api/products.ts
export async function onRequestGet(context: EventContext<Env, string, unknown>) {
const products = await context.env.DB.prepare("SELECT * FROM products").all();
return Response.json(products.results);
}
Branch preview deployments
Every PR automatically gets its own preview URL — useful for review before merging:
https://feat-new-checkout.my-app.pages.dev
Workers don't have this feature.
Pages Functions are Workers
A common misconception: Pages Functions are "less capable" than Workers. In reality, Pages Functions run on the same runtime as Workers — V8 isolates, edge network, same ability to bind KV/D1/R2/Queue.
The differences:
- Pages Functions route by filesystem (
functions/api/users.ts→/api/users) - Workers routing is handled entirely in your code
- Pages Functions don't support cron triggers
- Pages Functions can't receive Queue messages directly
If your app is Next.js on Pages, all API routes (app/api/) run on the Workers runtime — you can still bind D1, KV, and R2 via process.env or context.env.
When to use Workers, when to use Pages
Use Workers when
- Building a pure API backend with no frontend
- Need cron triggers to run on a schedule
- Need to receive and process Queue messages
- Writing a proxy or middleware that intercepts requests before origin
- Want full control over routing and logic without Pages' file-based structure
- Small edge scripts: image resizing, auth checks, geo-based redirects
Use Pages when
- Deploying a frontend framework: Next.js, Astro, SvelteKit, Remix
- Need static hosting + CDN for HTML/CSS/JS/images
- Want preview deployments per branch or PR
- The API is simple enough to live in
functions/alongside the frontend - Prefer deploying by git push over maintaining a separate CI/CD pipeline
Use both when
This is the most common pattern for real-world apps:
Pages (Next.js frontend)
└── bind KV, D1, R2
Workers (separate backend API)
└── bind same KV, D1, R2
└── cron triggers
└── Queue consumer
The frontend calls the Worker API via fetch("/api/...") or a separate domain. Both bind to the same KV namespace and D1 database.
Free tier compared
Workers free tier
- 100,000 requests/day — beyond this, $0.50 per million requests
- 10ms CPU time per request on the free plan (CPU time, not wall time)
- Cron triggers: 1 cron per Worker on the free plan
- Unlimited number of Workers
Pages free tier
- Unlimited requests — serving static files is completely free
- 500 builds/month (enough for a small team)
- 1 project on the free plan (unlimited on paid)
- Preview deployments: unlimited
- Pages Functions share the same quota as Workers — still counts toward the 100,000 requests/day limit
If your app is a pure static site with no Functions, Pages free tier is genuinely unlimited. The moment you add Functions, those requests count against the Workers limit.
Which one to choose
| What you're building | Use |
|---|---|
| API backend with no frontend | Workers |
| Background jobs, cron, Queue consumer | Workers |
| Proxy / edge middleware | Workers |
| Blog, docs site, landing page | Pages |
| Next.js / Astro / SvelteKit app | Pages |
| Full-stack with frontend + lightweight API | Pages (+ Functions) |
| Complex frontend + heavy backend | Pages + separate Workers |
If you're still unsure, a simple rule: if you have a frontend, use Pages; if you don't, use Workers. Pages Functions are capable enough for most lightweight backend needs, and moving a Function to a standalone Worker later is straightforward if the project grows.