The 10ms Budget: Architecting a One-Person Company's Website on Free Cloudflare
The 10ms Budget
_Or: how I decided what my server is allowed to do before I wrote a line of code._
The premise
I run a one-person company. My infrastructure budget is $0/month. My domain (openddd.com) lives on Cloudflare's free tier. That tier has a rule I only discovered after picking it: each request gets 10ms of CPU time on the Worker isolate before it's killed.
Ten milliseconds sounds tiny. It isn't — for a routing + templating workload. But it is tiny relative to what most backend code assumes it can spend. Most web handlers I've seen would blow through 10ms just in JSON parsing and ORM query building. So before writing any code, I drew a budget.
The budget
| Stage | Cost | Notes |
|---|---|---|
| Routing | 0.5ms | Hono's trie router is fast enough that we don't think about it |
| Cache lookup | 1ms | Isolate-local LRU; miss → hit D1, still under budget |
| Compute | 2ms | Business logic. This is the only line item allowed to grow |
| Serialization | 0.5ms | JSON.stringify on a 5–20KB payload |
| Transport+slack | 6ms | TLS, RTT within a PoP, event-loop jitter |
Total: 10ms. Any handler that goes over its compute line item gets pushed off the request path, not optimized in place.
The "端到端 10ms" claim would be physically unreachable — DNS alone costs more than that on a cold resolver. So we don't promise it. What we promise is server-side P99 under 10ms, which is what the CPU meter actually measures.
The rules that fall out of the budget
1. The request path only touches memory and local storage
No synchronous calls to external APIs. No Postgres round-trips. If a handler needs data from the outside world, that data is either already in the isolate (cache), or in D1 (SQLite at the edge), or it isn't needed for this response.
This is the reason D1 is on the map at all — its reads are colocated with the Worker in the same PoP. A 5ms D1 read at edge beats a 30ms Postgres read across the Pacific, even if Postgres is "faster" on paper.
2. The main database is only in the async write path
Every state change gets queued. A Durable Object or a Worker-with-KV queue holds the mutation, and a background Worker flushes to the canonical database. Reads never see the queue; they see the edge copy, which is eventually consistent.
For a personal-growth scoreboard or a content index — eventual consistency at seconds granularity is fine. For payments, it wouldn't be. So payments would go somewhere else (Stripe, with a webhook writing back into D1).
3. Language: TypeScript first, Rust+WASM second, Go never
TypeScript starts fastest and is the only runtime Cloudflare's free tier really optimizes for. When a compute-line-item exceeds budget (image processing, hashing, ML inference), the fallback is a WASM module written in Rust — those run inside the same isolate with no cold start.
Go is off the table because Cloudflare Workers doesn't accept Go binaries. If I ever need Go, I'll use Fastly Compute@Edge or move the specific function to a Cloudflare Worker-as-Container (paid tier only, and even that is still in preview as of this writing).
What "compute ≤ 2ms" actually rules out
Two milliseconds of compute is:
- Yes — parse 20KB JSON, run a regex, string-build an HTML fragment,
hash a token with crypto.subtle, do a 1000-element array reduce.
- No — compile a template on every request, iterate a 10K-row dataset,
do any synchronous network I/O, run an LLM.
Every "no" gets either cached, batched, moved to D1, or pushed to a Queue. That's it. There's no fourth option.
What this does NOT save you
The free tier caps you at:
- 100,000 requests/day. A crawler loop can burn that in an afternoon.
Mitigation: Bot Fight Mode is free and enabled by default; anything over quota falls back to static Pages.
- 1,000 KV writes/day. KV is a config store on this plan, not a
session store. If you need more writes, they go through D1 or Queues.
- 5GB D1 database, 5M row-reads/day. Fine for a personal-growth
scoreboard; not fine for a multi-tenant SaaS.
- 10GB R2 egress free. That's the ceiling on media hosting.
None of these limits bit me in the first week of building this site. They would bite around 500–1000 real users on a content product. At which point the whole thing has proven itself worth a $5/month Workers Paid upgrade.
What it saved
Zero dollars. That's the actual win. The 10ms budget forces me to write smaller, sharper handlers because I can't hide waste behind compute. It forces caching because the CPU clock won't tolerate a per-request DB hit. And it makes the deployment story boring: wrangler deploy, done, no container, no orchestrator, no cold-start tuning, no VPC peering.
For a one-person company, boring infrastructure is the highest-leverage feature you can buy. It's the difference between "spent the afternoon shipping content" and "spent the afternoon debugging a Kubernetes node pool".
What's next
The first thing this architecture powers is a .agent.md generator — a form that emits a valid YAML-frontmatter agent definition file you can drop into a Hermes-style workspace. The whole thing runs in one D1 read and one string-templating pass. Server CPU per request: measured at 0.4ms.
That's a small feature. But it's a live demonstration that this budget is workable, and every subsequent feature will have to justify exceeding it.