Skip to content

Cost and limits

Markdown

“Free hosting” is a claim worth being specific about, so here is the whole budget rather than the headline.

Static assets: 20,000 files per version on the free plan, 100,000 on Workers Paid.

Not bandwidth, not build minutes, not requests. A prerendered directory is a pile of files, and that pile is what runs out first. Everything else on this page is a footnote by comparison.

Static pages and assets are served by Cloudflare’s asset layer, are free and unmetered, and do not count against the Workers free plan’s 100,000 requests a day. A directory with no dynamic features never touches that quota at all, no matter how much traffic it gets.

Each listing costs a small, predictable number of files:

1 HTML page
1 Pagefind search fragment
1 OG image (when seo.ogImages: 'generated' and seo.ogScope.listings)
1 optimised logo (only when logos are served from your own R2 media domain)

Plus roughly 3,000 files of overhead — category, tag and pagination pages, generated pages up to pseo.maxPages, the Pagefind index chunks, the blog, category OG images and _astro/. The build fails above 90% of the plan limit and warns above 80%, so the usable budget on the free plan is 18,000 files.

Setup Files per listing Listings at $0/month
Logos from R2, OG images on 4 ~3,750
Direct logos, OG images on 3 ~5,000
Direct logos, seo.ogImages: 'static' 2 ~7,500

You do not have to take those numbers on trust. pnpm check:config prints your own before you build, and every build prints the actual count after:

budget: 145 listings × 3 files + 3000 overhead ≈ 3435 / 20000 files (free plan, 17%) — ok
headroom: up to ~5000 listings before the 90% limit on the free plan
[zerodirs:budget] budget: 781 files / 20000 (4%) — free plan, ok

The two numbers differ on purpose. The pre-build estimate budgets for programmatic SEO running all the way to its maxPages: 2000 cap; the demo actually generates 211 such pages, so the real total lands far below the estimate. Use the estimate to answer “can I add 2,000 listings?” and the build to answer “where am I actually?”. Both import their limits from the same module, so they cannot disagree about where the ceiling is.

Turning seo.ogScope.pseo on adds one PNG per generated page. That is the first switch to check if you are near the ceiling.

The assumption behind this table: 3,000 published listings, 5,000 daily visitors, 8,000 daily outbound clicks, 30 submissions and 10 approvals a day, 20 paid orders a month, click tracking on, OG images generated.

Resource Free allowance Where it goes Used
Static files 20,000 per version 3,000 × 4 + 3,000 overhead = 15,000 75% — this is the real ceiling
Worker requests 100,000 / day 8,000 click pings; ~600 submit; ~300 admin; ~5 webhooks; ~5,000 crawler probes on missing paths. Static pages and assets do not count ~14%
CPU per request 10 ms click: one upsert (<1 ms); webhook: HMAC + 2 statements (~2 ms); admin login: one HMAC (<1 ms); admin page: ≤7 queries. No password hashing, no runtime image work comfortable
D1 rows written 100,000 / day clicks 8,000 × ~2 = 16,000; submissions 30 × ~6 = 180; payments, audit, rebuild and outbox < 500 ~17%
D1 rows read 5,000,000 / day clicks read nothing; admin ~1,000; build-time REST ~3,000 rows × 7 builds <1%
D1 queries per invocation 50 admin ≤7; webhook 2; submit ≤5 fine
R2 10 GB, 1M Class A / month 30 uploads/day × 3 objects ≈ 3,000 Class A a month; reads go through a custom domain and Cloudflare’s cache <1%
Workers Builds 3,000 min/month, 20 min per build, 1 concurrent build per account ~7 builds a day at ~4 minutes ~28%
Email (Resend free) 3,000 / month and 100 / day 30 submissions × 2 + 10 approvals ≈ 70 a day ~70% of both
Email (Cloudflare, verified address) free on any plan, uncounted admin notifications when providers.adminNotify: 'cloudflare-verified' 0
Cron triggers 5 per account one hourly trigger 1
Cloudflare Access 50 free seats (Zero Trust free) one admin, if you put /admin/ behind it 1
WAF rate limiting exactly 1 rule on Free /admin/login/ 1
KV 1,000 writes / day not used — KV cannot be a counter at 1 write/s/key, so clicks go straight to D1 0

All of these have to hold at once:

  • ~3,500 listings with per-listing OG images and R2 logo variants; ~5,000 with OG images off; ~7,500 with direct logos as well.
  • ~40,000 outbound clicks a day — that is the D1 write limit, not the request limit.
  • ~100,000 dynamic requests a day, counting click pings, submissions, admin and the crawler traffic that hits paths with no file behind them.
  • Resend rather than Cloudflare Email, and ≤ 100 messages a day. The monthly figure averages to 100 a day, but the daily cap is enforced separately, so a launch spike stops at 100 even in a quiet month. The outbox retries the next day.
  • 10 ms of CPU per dynamic request, which is why the admin login is an HMAC comparison rather than a password hash.

Past any one of those: Workers Paid, $5 a month. It lifts every ceiling here at once — 100,000 files per version, 30 seconds of CPU per request, 10 million requests included.

The order of events is worth being clear about: you hit the file ceiling long before anything else, and only at a scale — several thousand listings — where $5 a month is not the interesting number in your business.

Set budget: { plan: 'paid' } so the build checks against the right limit. That field tells the build which ceiling to enforce; it does not change your Cloudflare billing, which you do in the dashboard.

What actually breaks when a quota runs out

Section titled “What actually breaks when a quota runs out”

The failure modes differ, and the differences matter more than the numbers.

  • Static pages stay up. They are unmetered. Exceeding a Workers quota does not take your site offline — pages, search index and images keep serving.
  • Click counts drop silently. Past the request quota, tracking calls fail. Outbound links still work, the visitor still reaches the destination; you just stop counting. Nothing alerts you, so treat popularity figures as approximate near the limit.
  • Email stops. Past 100 a day, Resend rejects. The submission is still stored; the notification is what is lost, and the outbox retries.
  • Builds fail and the last good version stays live. Out of build minutes, over the file budget, or tripping the orphan check — the deploy simply does not happen. That is the intended safety property, and it is why the build is strict rather than forgiving.

Two Cloudflare limits that surprise people

Section titled “Two Cloudflare limits that surprise people”

Build concurrency is account-wide. One concurrent build per account on the free plan, not one per Worker. Connect a second project and the two queue behind each other. If you run a demo site and a documentation site from the same account — as ZeroDirs itself does — their builds are serial.

Pagefind gets uncomfortable well before the file limit does. The search index is comfortable to roughly 10,000–20,000 pages. You will hit the 20,000-file ceiling first in most configurations, but if you have turned OG images off to buy headroom, search is the next thing to watch. See Search.