In short
- Static asset requests on Workers are free and unlimited. Only requests that run Worker code count towards 100,000 a day.
- Cloudflare now says to start new projects on Workers, not Pages. _headers and _redirects work the same in the asset folder.
- Workers needs your domain's nameservers on Cloudflare. Pages still accepts external DNS, the main reason left to pick it.
On this page
A personal site, portfolio, documentation or landing page built with a static site generator such as Astro, Hugo or Eleventy can get fast global hosting for nothing. You will get the current Workers free plan limits, a working wrangler.jsonc, a custom domain, security headers including a strict Content Security Policy and HSTS, caching and redirect rules, automatic deploys from GitHub, and what it would cost if traffic grew.
This site, techiarvind.com, is a static Astro build behind Cloudflare. It ships no JavaScript and sends a strict Content Security Policy; its _headers file, which works unchanged on Workers, is shown below.
Workers or Pages in 2026
Cloudflare has two products that serve static sites: Pages, the older one, and Workers with static assets. The Pages documentation now opens with a banner that is about as clear as vendor guidance gets: Workers “supports most Pages use cases and offers a broader feature set. It is Cloudflare’s primary platform for building applications. Start new projects with Workers.”
Pages is not deprecated, and existing projects keep working. Cloudflare publishes a migration guide from Pages to Workers, and its compatibility matrix shows the parts that matter for a static site:
_headersand_redirectsfiles are supported natively on Workers static assets, in the same place: the build output folder.- Preview URLs work on both.
- Branch deploy controls are only partly matched in Workers Builds, which “does not yet have the same level of configurability as Pages”.
- Workers custom domains need your domain’s nameservers on Cloudflare. Pages also supports domains whose DNS is hosted elsewhere.
That last point is the main reason left to choose Pages for a new static site. If your DNS is on Cloudflare, or can move there, use Workers.
What the free plan gives you
Checked on 11 November 2026.
| Item | Workers Free |
|---|---|
| Requests for static assets | Free and unlimited |
| Requests that run Worker code | 100,000 a day, reset at midnight UTC |
| CPU time per invocation | 10 ms |
| Asset files per Worker version | 20,000 (100,000 on Paid) |
| Size of one asset file | 25 MiB |
| Asset storage | No additional cost |
| Workers per account | 100 |
| Workers Builds (Git deploys) | 3,000 build minutes a month, 1 concurrent build, 20-minute timeout |
| Workers Logs | 200,000 log events a day, kept 3 days |
The first row is the one that matters. If your Worker has no script, every request is an asset request, and asset requests are not counted against the daily limit. A static site on Workers has no request bill, however many visitors it gets.
Set up Wrangler
Wrangler is Cloudflare’s CLI. Install it as a development dependency so CI uses the same version you do:
npm i -D wrangler@latest
npx wrangler login
npx wrangler whoami
wrangler login opens a browser for OAuth. Wrangler supports the Current, Active and Maintenance releases of Node.js.
Configure the Worker
Cloudflare recommends wrangler.jsonc for new projects. For a static site with a custom 404 page:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "my-site",
"compatibility_date": "2026-11-11",
"assets": {
"directory": "./dist",
"not_found_handling": "404-page",
"html_handling": "auto-trailing-slash"
}
}
The same in wrangler.toml, if you prefer it:
name = "my-site"
compatibility_date = "2026-11-11"
[assets]
directory = "./dist"
not_found_handling = "404-page"
html_handling = "auto-trailing-slash"
What each key does:
- No
mainkey: there is no Worker script. Requests that match a file get the file; nothing else runs. This is what keeps every request free. directory: your generator’s output folder (distfor Astro and Vite,publicfor Hugo,_sitefor Eleventy).not_found_handling: "404-page": serves your404.htmlwith a 404 status for unmatched paths. The alternative,single-page-application, servesindex.htmlfor unmatched paths, which is only right for client-side routed apps.html_handling: how trailing slashes and.htmlextensions are handled. The default,auto-trailing-slash, redirects/aboutto/about/when the file isabout/index.html, and servesabout.htmlat/about. The other values areforce-trailing-slash,drop-trailing-slashandnone.compatibility_date: set it to the day you create the project.
Preview locally, then deploy:
npm run build
npx wrangler dev
npx wrangler deploy
The first deploy gives you a *.workers.dev URL.
Add a custom domain
Add the domain to the config. The zone must be active on Cloudflare, and the hostname must not already have a CNAME record:
{
"name": "my-site",
"compatibility_date": "2026-11-11",
"assets": {
"directory": "./dist",
"not_found_handling": "404-page"
},
"routes": [
{ "pattern": "example.com", "custom_domain": true },
{ "pattern": "www.example.com", "custom_domain": true }
],
"workers_dev": false
}
On the next wrangler deploy, Cloudflare creates the DNS records and issues the certificates for you. You can do the same in the dashboard under the Worker’s Settings > Domains & Routes > Add > Custom Domain.
workers_dev: false turns off the *.workers.dev address, so search engines only see one copy of the site.
To send www to the bare domain, create a redirect rule using Cloudflare’s documented example: request URL https://www.*, target URL https://${1}, status 301, preserve query string.
Security headers with a _headers file
Create a plain text file called _headers, with no extension, so that it ends up in the asset directory. With Astro and most generators, put it in public/ and the build copies it to dist/. It is parsed, not served. The format is a URL pattern followed by indented Name: value lines.
This is the _headers file from the techiarvind.com repository:
/*
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-site
Permissions-Policy: accelerometer=(), autoplay=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), browsing-topics=()
Referrer-Policy: strict-origin-when-cross-origin
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
/_astro/*
Cache-Control: public, max-age=31536000, immutable
Why it can be this strict:
- The site ships no JavaScript and no inline styles. Astro is configured with
inlineStylesheets: "never", so every stylesheet is an external file from the same origin, andstyle-src 'self'holds without'unsafe-inline'. script-src 'self'rather than'none'keeps Cloudflare’s same-origin email decoding script working if Email Address Obfuscation is ever switched on for the zone.frame-ancestors 'none'andX-Frame-Options: DENYstop other sites framing the pages. The second covers older browsers.form-action 'none'andbase-uri 'none'close two injection routes that a static site never needs.
If you add analytics or a comments widget later, you will need to list its origins in script-src and connect-src. Add only what you use, and check the browser console for CSP violations after each change.
Headers are one layer. The zone settings in front of a Worker, such as SSL mode, WAF rules, rate limiting and bot settings, depend on your plan; Cloudflare security and performance settings for every plan goes through them.
Adding HSTS
HSTS tells browsers to use HTTPS for your domain for a set time, even if someone types http://. Add it to the /* block:
/*
Strict-Transport-Security: max-age=31536000; includeSubDomains
Or switch it on zone-wide in the dashboard under SSL/TLS > Edge Certificates > HTTP Strict Transport Security (HSTS), which sets it for the whole zone rather than one Worker.
Be deliberate about two things. Cloudflare warns that if HTTPS is removed while HSTS is active, the site is unreachable for the length of max-age. And the browser preload list requires a max-age of at least 31,536,000 seconds, includeSubDomains, the preload directive and an HTTP to HTTPS redirect. Preloading is hard to undo, so only add preload once every subdomain serves HTTPS.
Rules and limits for _headers
- Up to 100 header rules, and 2,000 characters per line.
*is a splat that matches greedily;:nameplaceholders also work.- Prefix a header name with
!to remove a header that a broader rule added. - Headers from
_headersare not applied to responses generated by Worker code. If you later add a script orrun_worker_first, set headers in the script for those routes.
Check the result after deploying:
curl -sI https://example.com/ | grep -i -E 'content-security-policy|strict-transport-security|x-frame-options'
Caching
By default, static assets are sent with Cache-Control: public, max-age=0, must-revalidate and an ETag. Browsers revalidate on every visit and get a cheap 304 when nothing changed. At Cloudflare’s side, assets are cached automatically at the location that served them, and tiered caching lets other locations fetch from a nearby cache rather than storage.
That default suits HTML: a new deploy shows up immediately. For files with a content hash in the name, such as Astro’s /_astro/ output, tell browsers to keep them for a year, as the file above does:
/_astro/*
Cache-Control: public, max-age=31536000, immutable
Never set long caching on HTML or on unhashed paths such as /favicon.ico or /og.png. If you change them, visitors keep the old copy.
Redirects with _redirects
Put a _redirects file next to _headers. Each line is source destination [status]:
# Old URL structure
/blog/* /blog/:splat 301
/cv /work/ 301
/posts/:slug /blog/:slug/ 301
What to know:
- Status codes 301, 302, 303, 307 and 308 are supported. The default is 302, so write 301 for permanent moves.
- Up to 2,000 static and 100 dynamic redirects (lines with splats or placeholders), 1,000 characters per line.
- Redirects run before assets are served and before headers are applied, so a redirect wins even if a file exists at the source path.
- Like
_headers, they do not apply to requests handled by Worker code.
Deploy from GitHub
You have two routes. Both are free for a site this size.
Workers Builds
In the dashboard, go to Workers & Pages > Create application, choose GitHub or GitLab and select the repository. For an existing Worker, use Settings > Builds > Connect. Each push to the production branch builds and deploys; other branches get preview builds with their own preview URL. The free plan includes 3,000 build minutes a month, one build at a time, with a 20-minute timeout. Build logs are kept for 90 days.
GitHub Actions
If you want builds next to your tests, or already have a pipeline, use Cloudflare’s official action. Create an API token from the Edit Cloudflare Workers template, then store it and your account ID as repository secrets named CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID.
name: deploy
on:
push:
branches: [main]
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run build
- uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy
Restrict the token to the one account it needs. For more on choosing and securing a pipeline, see CI/CD tools compared.
What it costs if traffic grows
Checked on 11 November 2026.
For a static site with no Worker script, nothing changes as traffic grows: asset requests stay free and unlimited, and storage has no extra cost. The limits you might meet first are file counts (20,000 per version on Free) and the 25 MiB per-file cap. Large downloads and video belong in object storage, not in the asset folder.
Costs start when you add code. A contact form handler, an API route or run_worker_first turns those requests into Worker invocations:
- Free plan: 100,000 invocations a day. Past that, Workers on routes either fail open (the request bypasses the Worker) or fail closed with error 1027, depending on the route setting. Requests matching
run_worker_firstpatterns get a 429 instead of falling back to the asset. - Paid plan: a $5 a month minimum per account, which includes 10 million requests and 30 million CPU milliseconds a month. Beyond that, $0.30 per additional million requests and $0.02 per additional million CPU milliseconds. Cloudflare states there are no extra charges for data transfer or bandwidth on the Paid plan.
A worked example: a site whose Worker code handles 50 million requests a month, averaging 2 ms of CPU each (an assumption; measure yours). Requests cost 40 million × $0.30 per million = $12. CPU is 100 million ms, minus 30 million included, so 70 × $0.02 = $1.40. With the $5 base, about $18.40 a month. Static asset requests on the same site still cost nothing.
Workers Builds on the Paid plan includes 6,000 build minutes a month, then $0.005 a minute.
When Workers is not the right host
- Your DNS must stay with another provider. Workers custom domains need Cloudflare nameservers; Pages does not.
- You publish more than 20,000 files per deploy on the free plan.
- You serve large media files over 25 MiB each.
- The site is mostly server-rendered. Every page view becomes a Worker invocation; still cheap, but no longer free and unlimited.
If you need monitoring for the servers behind any dynamic parts, the New Relic free tier guide covers it.
Deploy checklist
- Move the domain’s DNS to Cloudflare, or use Pages if it cannot move.
- Install Wrangler as a dev dependency and log in.
- Write
wrangler.jsoncwithassets.directory,not_found_handlingand nomain. - Add the apex and
wwwas custom domains, redirectwwwwith a redirect rule, and setworkers_dev: false. - Add
_headerswith a CSP that matches what the site actually loads, plus HSTS. - Give hashed assets a one-year immutable cache; leave HTML on the default.
- Move old URLs with 301s in
_redirects. - Deploy on push with Workers Builds or
cloudflare/wrangler-action. - Keep the site script-free and the bill stays at zero.
Sources
- Workers pricing developers.cloudflare.com
- Workers limits developers.cloudflare.com
- Static assets developers.cloudflare.com
- Static assets: billing and limitations developers.cloudflare.com
- Static Site Generation and custom 404 pages developers.cloudflare.com
- HTML handling developers.cloudflare.com
- Headers (static assets) developers.cloudflare.com
- Redirects (static assets) developers.cloudflare.com
- Migrate from Pages to Workers developers.cloudflare.com
- Cloudflare Pages documentation developers.cloudflare.com
- Wrangler configuration developers.cloudflare.com
- Install and update Wrangler developers.cloudflare.com
- Wrangler general commands developers.cloudflare.com
- Custom Domains developers.cloudflare.com
- Redirect www to root developers.cloudflare.com
- Workers Builds developers.cloudflare.com
- Workers Builds limits and pricing developers.cloudflare.com
- Deploy Workers with GitHub Actions developers.cloudflare.com
- HTTP Strict Transport Security (HSTS) developers.cloudflare.com
- HSTS preload list submission requirements hstspreload.org