Hosting a static site for free on Cloudflare: Workers or Pages?

A comparison of hosting a static site on Cloudflare Workers and Pages on the free plans, covering trade-offs, room to grow, and how to choose.

Putting a front-end static site on Cloudflare without paying leaves two options: Pages, and Workers with Static Assets. This note assumes the free plans and compares the two on quotas, trade-offs, and what changes once server-side code, a database or a paid plan comes in, then ends with conditions that can be checked directly.

Limits and prices are as of November 2026 per the official docs.

The scenario

The build produces a folder (for example dist/) that holds only HTML, CSS, JavaScript and images, with no code that has to run on a server. The goal: deploy automatically on every Git push, use a custom domain, and pay nothing each month.

Both products can do this. They differ in how quotas are counted, how previews are set up, what the domain has to be, and how much room there is once the site grows.

What each one is

Cloudflare Pages

Pages (external link) is Cloudflare’s dedicated site hosting product. The usual setup is to connect a GitHub or GitLab repo in the dashboard, set a build command and an output folder, and from then on every push builds and deploys automatically (Git integration (external link)). Every deployment gets its own <hash>.<project>.pages.dev URL, and every branch gets a <branch>.<project>.pages.dev alias, with no extra setup; branch aliases are lowercased and non-alphanumeric characters become hyphens, so fix/api becomes fix-api (Preview deployments (external link)).

Without Git, Direct Upload (external link) works too: drag and drop up to 1,000 files in the dashboard, or upload up to 20,000 files with Wrangler1, each file up to 25 MiB. For server-side code, Pages Functions go in a /functions folder, and the file names decide the routes; for example, /functions/fruits/apple.js serves /fruits/apple (Functions routing (external link)).

Workers with Static Assets

Workers runs code on Cloudflare’s edge. Static Assets (external link) lets static files be uploaded and deployed together with a Worker.

Workers Static Assets

When a request path matches a static file, that file is returned without running any Worker code. When no file matches and there is no Worker code, the response is a 404. With no main (the Worker code entry point) in the config, the whole site is static files only; the official static site example (external link) config has no main either.

Git deploys are handled by Workers Builds (external link): connect the repo and set a build command; the deploy command defaults to npx wrangler deploy (Builds configuration (external link)), and every push to the production branch builds and deploys2. Workers Builds connects to Artifacts, GitHub, GitLab and Cursor Origin repos, and does not support self-hosted GitHub or GitLab; other providers (such as Bitbucket) can use an external CI/CD service (such as GitHub Actions) that deploys with Wrangler (Workers Builds Git integration (external link)).

A purely static site on Workers needs only one config file:

wrangler.jsoncjsonc
{
  "name": "my-site",
  "compatibility_date": "2026-11-04",
  // No "main": no Worker code runs, static files answer every request
  "assets": {
    "directory": "./dist",
    // When a path is not found, return the nearest 404.html with status 404
    "not_found_handling": "404-page"
  }
}

not_found_handling accepts "single-page-application", "404-page" or "none", and the default is "none" (Wrangler configuration (external link)). Coming from Pages, the main change is how the output folder is declared. Pages detects the 404 and single-page application (SPA) behavior by itself, while Workers needs it spelled out (migration guide (external link)):

wrangler.jsoncDiff
 {
   "name": "my-site",
   "compatibility_date": "2026-11-04",
-  "pages_build_output_dir": "./dist"
+  "assets": {
+    "directory": "./dist",
+    "not_found_handling": "404-page"
+  }
 }

The two deploy flows look roughly like this:

Deploy flows on Pages and Workers. Static asset requests are free on both; requests that run Functions or Worker code count toward the daily quota.Pages: git push (GitHub / GitLab), then Pages build (500 builds / month on the free plan), then pages.dev and custom domains (a subdomain can stay on external DNS). Workers + Static Assets: git push (4 Git providers), then Workers Builds (3,000 minutes / month on the free plan), then workers.dev and custom domains (the domain must be on Cloudflare). All lead to Static asset requests (free and unlimited on both).git pushGitHub / GitLabPages build500 builds / month onthe free planpages.dev and customdomainsa subdomain can stay onexternal DNSgit push4 Git providersWorkers Builds3,000 minutes / month onthe free planworkers.dev and customdomainsthe domain must be onCloudflareStatic asset requestsfree and unlimited on bothPagesWorkers + Static AssetsDeploy flows on Pages and Workers. Static asset requests are free on both; requests that run Functions or Worker code count toward the daily quota.Pages: git push (GitHub / GitLab), then Pages build (500 builds / month on the free plan), then pages.dev and custom domains (a subdomain can stay on external DNS). Workers + Static Assets: git push (4 Git providers), then Workers Builds (3,000 minutes / month on the free plan), then workers.dev and custom domains (the domain must be on Cloudflare). All lead to Static asset requests (free and unlimited on both).git pushGitHub / GitLabPages build500 builds / month on the freeplanpages.dev and custom domainsa subdomain can stay on externalDNSgit push4 Git providersWorkers Builds3,000 minutes / month on thefree planworkers.dev and custom domainsthe domain must be on CloudflareStatic asset requestsfree and unlimited on bothPagesWorkers + Static Assets
Deploy flows on Pages and Workers. Static asset requests are free on both; requests that run Functions or Worker code count toward the daily quota.

The _headers file has the same format on both: it has to end up at the root of the output folder (for example dist/); with a framework it usually goes in public/, which the build copies there. In the file, a path goes on the first line and the headers indented below it, with up to 100 rules at up to 2,000 characters per line. It applies only to static asset responses; responses generated by Pages Functions or Worker code need their headers set in that code (Workers headers (external link), Pages headers (external link)).

public/_headersText
/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin

/assets/*
  Cache-Control: public, max-age=31536000, immutable

Replace /assets/* in the example with the folder where the framework actually writes hashed file names.

Free plan comparison table

Item (free plan)PagesWorkers + Static Assets
Static asset requestsFree, unlimitedFree, unlimited
Requests that run server-side codePages Functions count toward the Workers Free daily quota100,000 per day, shared across the account (and with Pages Functions)
Files20,000 per site20,000 per Worker version
Size of one file25 MiB25 MiB
Build allowance500 builds per monthWorkers Builds: 3,000 minutes per month
Concurrent builds1 per account1 per account
Build timeout20 minutes20 minutes
Per account100 projects100 Workers
Custom domains100 per project; a subdomain can stay on external DNS (CNAME), an apex domain must be a Cloudflare zoneThe domain must be a Cloudflare zone; 100 per zone (counted per zone, not per project)
PreviewsCreated automatically for every deployment and branch, unlimitedWorker Previews, 100 per Worker, needs setup
Cron TriggersNot supported5 per account
LogsReal-time logsReal-time logs, Workers Logs (200,000 events per day, kept 3 days)

Sources for the numbers: Pages limits (external link), Pages custom domains (external link), Pages Functions pricing (external link), Workers limits (external link), Workers Builds limits (external link), Worker Previews (external link), Workers custom domains (external link) and Workers pricing (external link). Feature support comes from the migration guide’s matrix (external link).

Static asset requests are not billed

Both sets of docs call static asset requests “free and unlimited” (Pages Functions pricing (external link), Workers pricing (external link), Static Assets billing and limitations (external link)). No official page says word for word that requests served only by static files are left out of the 100,000 per day, but the Pages docs define a static request as one that does not invoke Functions, and the Workers docs say only requests that run Worker code are billed. A purely static site therefore normally never reaches the daily quota.

Trade-offs and room to grow

Everything below assumes the free plans; the quota numbers are in the comparison table above.

Pages

  • Advantages: preview URLs for branches and for every deployment are created automatically, with no config to write; a folder can be dragged into the dashboard and go live; when the site lives on a subdomain (such as www.example.com), the domain’s DNS can stay with another provider and point to Pages with a CNAME, while an apex domain (such as example.com) still has to be added as a Cloudflare zone with its nameservers moved to Cloudflare (Pages custom domains (external link))3; in the migration guide’s matrix (external link), external domains are the only feature that Pages supports and Workers does not, and four more (Early Hints, Branch Deploy Controls, file-based routing, Pages Plugins) are only partially supported on Workers; 404 and SPA behavior are detected automatically; dashboard users are unlimited (Pages limits (external link)).
  • Disadvantages: the build allowance is counted per build, so every push counts once however short the build is; Git integration (external link) supports only GitHub and GitLab, not self-hosted instances, and other providers use Direct Upload from CI; projects created with Git integration and with Direct Upload cannot be switched to the other later (Direct Upload (external link), Git integration (external link)), though a Git-integrated project can pause automatic deployments with branch control and deploy directly with Wrangler instead (Git integration (external link)); preview URLs are public by default; “Enable access policy” limits them to members of the Cloudflare account by default, and Access policies can add other people. It covers previews only; the *.pages.dev production URL and custom domains need separate, manual Access setup per the docs’ Known issues4; the official docs recommend Workers for new projects (external link), and Cloudflare has said on its blog (external link) that future investment, optimizations and new features go to Workers.
  • As needs grow: for an API or form handling, add Pages Functions under /functions. Their requests count toward the Workers Free daily quota and are billed the same way as Workers (Pages Functions pricing (external link)). For a database, bindings such as D1 and KV are available, but a Durable Object cannot be created inside a Pages project; it has to be deployed as a separate Worker and bound in (Pages Functions bindings (external link)). Cron Triggers, Queue consumers, Workers Logs and Logpush are not supported (migration guide (external link)). Beyond 20,000 files, the site has to move to a Pro, Business or Enterprise plan and set PAGES_WRANGLER_MAJOR_VERSION=4 in the project to get 100,000 files per site; those plans also allow more builds, 5,000 a month on Pro and 20,000 on Business (Pages limits (external link)). The Pages limits page does not say that Workers Paid raises this limit. When these features are needed, Cloudflare provides a guide for moving from Pages to Workers (external link).

Once Functions are added, limit them with _routes.json

When a project has a functions folder, Pages generates _routes.json at deploy time to decide which requests run Functions. Written by hand, it can let only /api/* run code while every other request stays a free static asset request; paths that are not listed in include, or are excluded by exclude, do not run Functions. _routes.json needs at least one include rule, allows at most 100 include and exclude rules combined at up to 100 characters each, and exclude takes priority (Functions routing (external link)).

_routes.jsonJSON
{
  "version": 1,
  "include": ["/api/*"],
  "exclude": []
}

Workers with Static Assets

  • Advantages: the build allowance is counted in minutes, so a site with short builds can build many times; the config lives in wrangler.jsonc and is versioned with the code; server-side code, Cron Triggers or queues can be added to the same project later without changing platforms; turning on “Protect this Worker behind Access” on the Worker’s Access tab with the scope set to “All traffic” protects every URL of the Worker from one setting, including routes, custom domains, workers.dev and preview URLs5; it is the approach Cloudflare recommends for new projects (external link).
  • Disadvantages: a custom domain must be a Cloudflare zone (the docs require “An active Cloudflare zone”), which means the domain’s nameservers have to be managed by Cloudflare, for subdomains too (Workers custom domains (external link), migration guide (external link)); branch previews need Worker Previews set up, and preview URLs are public by default6; the workers.dev route and Version URLs are public when enabled (see the note below); a request with a Range header gets the full file with 200 OK instead of 206 Partial Content, which can break uses such as video playback in Safari, and the documented workaround is run_worker_first with env.ASSETS.fetch() (Static Assets billing and limitations (external link)).
  • As needs grow: for an API or form handling, add main to the config and write Worker code. Requests still look for a static file first and run the code only when nothing matches. Requests that run code count toward the 100,000 per day, each request gets at most 10 ms of CPU time, and going over the daily quota returns Error 1027, with the quota reset at midnight UTC; each route can be set to bypass the Worker when over the quota (fail open) or show the 1027 error page (fail closed) (Workers limits (external link)). Databases and other services work on the free plan: D1 (external link) allows 5 million rows read and 100,000 rows written per day, with 5 GB in total, and once the daily read or write limit is reached, queries fail until the reset at 00:00 UTC; Durable Objects (external link) are limited to the SQLite backend, with 100,000 requests and 13,000 GB-s per day, 5 million rows read and 100,000 rows written per day, and 5 GB in total, and operations past a limit fail; Queues (external link) allow 10,000 operations per day. With more than 20,000 files, or more than 100,000 requests a day that run code, moving to Workers Paid (at least USD 5 per month per account) raises the file limit to 100,000 per version and removes the daily request limit (Workers pricing (external link), Workers limits (external link)).

Does turning off workers.dev also turn off Version URLs?

The workers.dev page (external link) says disabling the workers.dev route does not disable Version URLs, while the Version URLs page (external link) says the Version URL default follows workers_dev (“If workers_dev is disabled, Version URLs are disabled by default”), and that when preview_urls is left out of the config, Wrangler does not change an existing setting. Setting preview_urls explicitly in the config, rather than relying on the default, is therefore advisable. The migration guide (external link)’s Worker Previews example uses "preview_urls": true together with a previews block.

run_worker_first paths count toward the quota

With assets.run_worker_first set, matching paths always run the Worker first. The setting is a boolean or an array of route patterns (with ! exceptions, at most 100 entries), and defaults to false (Wrangler configuration (external link)). Once the free plan’s daily quota is used up, those requests get a 429 instead of falling back to the static file; paths that do not match, and paths excluded with !/..., are unaffected (Static Assets billing and limitations (external link)).

Side by side as the site grows

NeedPagesWorkers + Static Assets
API, form handlingPages Functions (file-based routes in /functions)Worker code (add main to the config)
Cron TriggersNot supported5 per account on the free plan
DatabaseD1, KV and other bindings; Durable Objects need a separate WorkerD1, KV and Durable Objects all work directly
QueuesProducer onlyProducer and consumer; Workers Paid includes 1 million operations per month, then USD 0.40 per million
LogsReal-time logsAlso Workers Logs, Logpush, Tail Workers, Source Maps; starting December 1, 2026, Workers Logs moves to Cloudflare Observability pricing
Files100,000 per site on Pro, Business or Enterprise once the environment variable is set100,000 per version on Workers Paid
Paid planFunctions are billed at Workers rates; the file and build limits rise only on Pro or higher plansAt least USD 5 per month, including 10 million requests and 30 million CPU ms; then USD 0.30 per million requests and USD 0.02 per million CPU ms, with no egress or bandwidth charges

Feature support comes from the migration guide (external link) and Pages Functions bindings (external link), the per-plan Pages limits from Pages limits (external link), Workers prices and log pricing from Workers pricing (external link) and Workers limits (external link), and queue prices from Queues pricing (external link).

Pages stays supported; new features go to Workers

In a blog post on April 8, 2025 (external link), Cloudflare said Pages will continue to be supported, while future investment, optimizations and new features go to Workers. The Pages docs home page (external link) has a section titled “Are you sure you want to use Pages?” whose banner says “Start new projects with Workers”; Workers Best Practices (external link) says “If you are starting a new project, use Workers instead of Pages” and calls Static Assets the recommended way to deploy static sites. Neither the post nor the docs give an end-of-service date for Pages.

How to choose

Pages is worth considering when:

  • The site lives on a subdomain (such as www.example.com) whose DNS is not on Cloudflare, and moving the nameservers is not planned. An apex domain needs a Cloudflare zone on Pages too.
  • Every branch and every deployment needs a preview URL, without writing a config file.
  • The goal is to drag a folder into the dashboard and go live, with no more than 1,000 files.
  • A Pages project is already running and uses nothing that only Workers has, such as Cron Triggers, Queue consumers or Workers Logs.

Workers with Static Assets is worth considering when:

  • It is a new project and the domain is already on Cloudflare.
  • Pushes would trigger more than 500 builds a month: Workers Builds counts minutes, so at 2 minutes per build, 3,000 minutes is about 1,500 builds.
  • Cron Triggers, Queue consumers or Durable Objects are planned, or Workers Logs is needed for debugging.
  • Cloudflare Access has to protect every URL of the site (including the production URL and custom domains), not only preview URLs: on Workers, turning on Access at the Worker level with “All traffic” protects the whole Worker however it is reached (Cloudflare Access on Workers (external link)); the Zero Trust free plan (external link) allows up to 50 users. Pages can do it too, but the production URL needs the generated Access application edited, and custom domains need their own Access application (Known issues (external link)).

Worth watching on both:

  • The file count is close to 20,000: the free limit is the same on both, but going beyond it takes different plans: Pages needs a Pro or higher plan, and Workers needs Workers Paid (Pages limits (external link), Workers limits (external link)).
  • Requests that run server-side code would exceed 100,000 per day: Pages Functions and Workers share the same quota, so switching platforms adds nothing (Pages Functions pricing (external link)).
  • Choosing Workers for a site with video that has to play in Safari: Workers static assets do not return 206 Partial Content, so those paths need run_worker_first and Worker code to handle Range requests; those requests run the Worker, and on the free plan they get a 429 once the daily quota is exceeded (Static Assets billing and limitations (external link)).

What the docs do not say

Neither set of docs says what happens when the 3,000 Workers Builds minutes or the 500 Pages builds run out (blocked, queued or charged). The Pages docs home page (external link) says “500 deploys per month” while the limits page (external link) says 500 builds, and neither says whether Direct Upload (external link) counts toward it.

Wrap-up

  • A new static site on a domain that is on Cloudflare: use Workers with Static Assets. Static asset requests are just as free, code, Cron Triggers or a database can be added later without changing platforms, and it is the direction Cloudflare recommends.
  • A site on a subdomain whose DNS is not on Cloudflare and will not move, or only zero-setup branch previews and drag-and-drop uploads needed: use Pages.
  • Already on Pages with static files only: it can stay there, and move to Workers with the migration guide once Cron Triggers, Queue consumers or Workers Logs are needed.

Notes

  1. Wrangler is the command-line interface (CLI) for the Cloudflare Developer Platform and manages Workers projects; Cloudflare recommends installing it locally in each project (npm i -D wrangler@latest) rather than globally (install). wrangler dev starts a local server for developing a Worker, and wrangler deploy deploys it to Cloudflare (Workers commands); wrangler pages deploy deploys a folder of static assets as a Pages deployment (Pages commands). The config file can be JSON (wrangler.json or wrangler.jsonc) or TOML (wrangler.toml); Cloudflare recommends wrangler.jsonc for new projects, and some newer features work only with a JSON config (configuration).

  2. When “Create new token” is selected, Workers Builds creates an API token automatically, and its permissions can be changed later under My Profile > API Tokens; by default it has Account Settings read, Workers Scripts edit, Workers KV Storage edit, Workers R2 Storage edit, Workers Routes edit on all zones, and User Details and Memberships read. See Builds configuration. The Worker name in the dashboard has to match name in the config; see Builds troubleshooting.

  3. The docs warn that adding the CNAME by hand without first adding the domain in the dashboard leaves the domain failing to resolve, possibly with a 522 error.

  4. The docs say “By default, these deployment URLs are public”, and that the access policy “will only protect your preview deployments”; see Preview deployments. Protecting *.pages.dev means editing the generated Access application, and each custom domain needs a new self-hosted Access application in Zero Trust with the domain added as its public hostname; the steps are in Known issues.

  5. The scope can be “Previews only” or “All traffic”; Zero Trust has to be enabled on the account, and WebSockets are not supported. The feature launched on August 14, 2026; see the changelog. For setup, see Cloudflare Access on Workers.

  6. Worker Previews launched on September 22, 2026 (see the changelog), need Wrangler 4.135.0 or later, and are created with npx wrangler preview. The config needs a previews block (it can be empty), and previews do not inherit production vars, secrets or bindings. Preview builds in Workers Builds are toggled with “Enable Preview Builds” under Settings > Build > Branch control. See Worker Previews, its configuration and Build branches.

That is the end of this note. Thanks for reading.

閱讀中文版