Cost and limits
“Free hosting” is a claim worth being specific about, so here is the whole budget rather than the headline.
The one limit that matters
Section titled “The one limit that matters”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.
How many listings fit
Section titled “How many listings fit”Each listing costs a small, predictable number of files:
1 HTML page1 Pagefind search fragment1 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%) — okheadroom: up to ~5000 listings before the 90% limit on the free plan[zerodirs:budget] budget: 781 files / 20000 (4%) — free plan, okThe 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 full budget at 3,000 listings
Section titled “The full budget at 3,000 listings”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 |
Where $0 stops
Section titled “Where $0 stops”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.