# Cost and limits

> What $0 a month actually buys on Cloudflare's free plan, which limit you hit first, the full quota budget at 3,000 listings, and what breaks when a quota runs out.

You are reading one page of the ZeroDirs documentation. ZeroDirs is a paid Astro + Cloudflare Workers starter for directory sites: every page is rendered to static HTML at build time, the site ships no client JavaScript outside `/search/`, and one file — `site.config.ts` — carries roughly 80% of the customisation.

Two things to hold on to before you act on anything below:

- `site.config.ts` is validated by a zod schema with ten cross-field rules. After any edit to it, run `pnpm check:config`; every problem is reported as `site.config.ts › <path>: <message>` and the whole file is checked at once.
- The repository ships its own `AGENTS.md` with twenty hard rules, and a machine-checked test suite behind them. If you are working inside a ZeroDirs project, read that file first — it overrides anything general you infer from this page.

Source: https://zerodirs.com/docs/run/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

**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

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 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

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

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

**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](https://zerodirs.com/docs/configure/search/).

:::note[The copy you receive goes further]
`docs/limits.md` inside the starter is the private twin of this page, with the reasoning behind
the two budget numbers and why `wrangler deploy` reports a larger file count than the build does
(it counts directories too).
:::
