Teams adopt Pages because the git push flow with automatic build and instant deploy is genuinely simpler than building a CI pipeline from scratch. That's a real advantage, not marketing. The problem appears when the project grows and you start to hit limits that Pages cannot solve: cron triggers, multiple Workers with separate responsibilities, staging with different bindings. At this point, migrating to Workers seems obvious — but the true cost of it is rarely calculated before you start.
The real migration triggers
The most common trigger is cron triggers. Pages does not support cron natively. If you need a job that runs periodically — processing payments, synchronizing external data, generating scheduled reports — you already have a separate Worker running alongside your Pages project. As the number of satellite Workers grows, the team starts to wonder if it would make more sense to consolidate everything into Workers.
The second trigger is deployment fragmentation. A Pages project is a monolithic deployment unit — frontend, Functions, all together. When you want to divide responsibilities between specialized Workers (one for the public API, one for internal processing, one for webhooks), Pages doesn't offer that granularity. Deploying a change to the webhooks handler triggers a rebuild of the entire frontend.
The third is staging with production parity. Pages have production and preview environments, but they do not have named environments with completely different bindings. If you want a staging D1 database completely separate from the production one, with different environment variables and perhaps even a different KV model, the solution in Pages is to create a separate Pages project and manage the synchronization manually. In Workers, this is in wrangler.toml with blocks [env.staging] and [env.production].
What you lose when you leave Pages
Before migrating, the correct calculation is to list what Pages offers that you will need to replace.
The build pipeline is the most underrated item. Pages connects to the repository, detects the framework, runs the build, and uploads the assets. In Workers, you take on this responsibility: own CI/CD (GitHub Actions, GitLab CI, whatever), build script, and uploading assets to R2 if you still need to serve static files.
Preview deployments per branch are the second item. Pages automatically generates a preview URL for each branch — push, available URL, comment on PR. Replicating this in Workers requires work: a CI script that detects the branch name, deploys with a Worker name derived from the branch (minha-api-pr-247), and comments the URL in the PR via the GitHub API. It works, but it's infra code that you write and maintain.
The third item is the CDN for static assets. This is the most expensive to ignore.
The account of static assets
In Pages, static assets (HTML, CSS, JS, images) are served directly from the Cloudflare CDN without triggering the Workers runtime. Each request for a static asset costs $0 per request, regardless of volume.
In Workers, you don't have this primitive natively. To serve assets, you need R2 (object storage) and logic in the Worker that searches for the correct asset, applies cache headers and returns the content. Each request that passes through the runtime counts as a Worker invocation.
Workers' paid plan includes 10 million requests per month and charges $0.30 per million above that. A website with 100 million monthly requests, 90 million of which are for static assets: on Pages, the request cost is $0. In Workers, there are 90 million invocations above the included package — $27/month for this delta alone, growing linearly with traffic.
For a website with 500 million monthly requests with 85% static assets: the difference amounts to more than $120/month. The cost of the Pages pro plan is $20/month. Migrating to Workers, in this scenario, is a decision that increases operational costs in exchange for flexibility.
The wrangler.toml to serve R2 assets
If the migration makes sense despite the cost, the correct pattern for serving static assets in Workers uses R2 with Cache API:
# wrangler.toml name = "meu-site" main = "src/index.ts" compatibility_date = "2024-09-24" [[r2_buckets]] binding = "ASSETS" bucket_name = "meu-site-assets" [[routes]] pattern = "meusite.com/*" zone_name = "meusite.com"
// src/index.ts export default { async fetch(request: Request, env: Env): Promise<Response> { const cache = caches.default; const cached = await cache.match(request); if (cached) return cached; const url = new URL(request.url); const key = url.pathname.slice(1) || "index.html"; const object = await env.ASSETS.get(key); if (!object) { return new Response("Not Found", { status: 404 }); } const response = new Response(object.body, { headers: { "Content-Type": object.httpMetadata?.contentType ?? "application/octet-stream", "Cache-Control": "public, max-age=31536000, immutable", }, }); await cache.put(request, response.clone()); return response; }, };
This replicates the CDN behavior of Pages, but you are paying for each cache miss (first request per file per Cloudflare PoP). The Pages CDN has this caching layer at no additional cost per request.
How to migrate without downtime
The least risky sequence: first, keep the Pages project running. Create new Workers in parallel. Configure routes that send traffic to new Workers for specific paths (starting with the lowest risk ones, such as webhooks or admin routes). Validate the behavior in production with real traffic before moving the main paths. Only then deactivate the corresponding Pages Functions.
For the frontend, do not migrate Pages assets to R2 until you are sure the additional cost is within the project budget. In many cases, the hybrid architecture — Pages for frontend and assets, Workers for asynchronous jobs and specialized services — is cheaper and equally flexible than a full migration.
What migration really solves
Cron triggers, multiple Workers with granular routing, staging with completely isolated environments — these are the real problems that justify migration. If you are migrating for another reason, it is worth checking to make sure you are not exchanging a limitation for a higher cost.
Workers' flexibility comes at a concrete operational price: you assume the CI pipeline, preview deployment, and the cost of serving static assets. For pure backend services without significant assets, this cost is low. For applications with a heavy frontend and high volume of static traffic, it can be substantial.
Also read
- Cloudflare Workers vs Pages: the difference that matters before you choose
- What only Workers do, what only Pages do and where the two meet
- Workers and Pages: deployment, routing and what each model hides
- Pages Functions: when to use instead of pure Workers
- When Durable Objects are the wrong answer
- DNS proxied vs DNS only: what changes and when each mode makes sense
