Arvind Verma

Host a website for free on Cloudflare Workers (static assets)

Host a low-traffic static site free on Cloudflare Workers: plan limits, wrangler.jsonc, custom domain, _headers with CSP and HSTS, redirects and CI deploys.

By Arvind Verma9 min read

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
  1. Workers or Pages in 2026
  2. What the free plan gives you
  3. Set up Wrangler
  4. Configure the Worker
  5. Add a custom domain
  6. Security headers with a _headers file
  7. Caching
  8. Redirects with _redirects
  9. Deploy from GitHub
  10. What it costs if traffic grows
  11. When Workers is not the right host
  12. Deploy checklist

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:

  • _headers and _redirects files 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 main key: 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 (dist for Astro and Vite, public for Hugo, _site for Eleventy).
  • not_found_handling: "404-page": serves your 404.html with a 404 status for unmatched paths. The alternative, single-page-application, serves index.html for unmatched paths, which is only right for client-side routed apps.
  • html_handling: how trailing slashes and .html extensions are handled. The default, auto-trailing-slash, redirects /about to /about/ when the file is about/index.html, and serves about.html at /about. The other values are force-trailing-slash, drop-trailing-slash and none.
  • 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, and style-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' and X-Frame-Options: DENY stop other sites framing the pages. The second covers older browsers.
  • form-action 'none' and base-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; :name placeholders also work.
  • Prefix a header name with ! to remove a header that a broader rule added.
  • Headers from _headers are not applied to responses generated by Worker code. If you later add a script or run_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_first patterns 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.jsonc with assets.directory, not_found_handling and no main.
  • Add the apex and www as custom domains, redirect www with a redirect rule, and set workers_dev: false.
  • Add _headers with 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

  1. Workers pricing developers.cloudflare.com
  2. Workers limits developers.cloudflare.com
  3. Static assets developers.cloudflare.com
  4. Static assets: billing and limitations developers.cloudflare.com
  5. Static Site Generation and custom 404 pages developers.cloudflare.com
  6. HTML handling developers.cloudflare.com
  7. Headers (static assets) developers.cloudflare.com
  8. Redirects (static assets) developers.cloudflare.com
  9. Migrate from Pages to Workers developers.cloudflare.com
  10. Cloudflare Pages documentation developers.cloudflare.com
  11. Wrangler configuration developers.cloudflare.com
  12. Install and update Wrangler developers.cloudflare.com
  13. Wrangler general commands developers.cloudflare.com
  14. Custom Domains developers.cloudflare.com
  15. Redirect www to root developers.cloudflare.com
  16. Workers Builds developers.cloudflare.com
  17. Workers Builds limits and pricing developers.cloudflare.com
  18. Deploy Workers with GitHub Actions developers.cloudflare.com
  19. HTTP Strict Transport Security (HSTS) developers.cloudflare.com
  20. HSTS preload list submission requirements hstspreload.org