The $0 Challenge
When I started building PriceGuess Israel, I had one constraint: no monthly costs. Not because I couldn't afford a $5/month VPS, but because I wanted to prove that modern edge computing platforms offer enough in their free tiers to run a real production application. Running PriceGuess in production with real-time multiplayer, a daily scraper, and a D1 database, my hosting bill is exactly $0.
Here's what the Cloudflare free tier actually includes, what I've hit the limits on, and the architectural decisions I made to stay within bounds.
What Cloudflare Gives You for Free
The free tier is surprisingly generous for the kind of app PriceGuess is:
- Workers - 100,000 requests/day, 10ms CPU time per request. For SSR pages and API routes, this is plenty.
- D1 Database - 5 million row reads/day, 100,000 row writes/day, 5GB storage. Our daily challenge data, user profiles, and game history fit comfortably.
- Durable Objects - 1 million requests/month to DO instances. Each 1v1 game room uses roughly 50-100 requests (connect, guesses, results), so this supports thousands of games per month.
- CRON Triggers - Free, runs up to once per minute. Our scraper runs hourly.
- Custom domains - Unlimited, with free SSL. Both price.etiel.dev and www.etiel.dev are custom domains on the free plan.
Where I Had to Be Clever
1. CPU Time: The Real Bottleneck
The 10ms CPU time limit per request is the tightest constraint. This means every Worker request - including SSR rendering - must complete its CPU work within 10 milliseconds. Wall clock time (waiting for database queries) doesn't count, only computation.
For Astro SSR, this required careful optimization. I had to avoid heavy computation in server-side code. Template rendering with simple data binding is fast enough, but things like JSON parsing of large datasets or complex string manipulation can blow past 10ms. My rule: fetch data, then template it. No transformation pipelines.
2. D1: Write Carefully, Read Freely
With 100,000 writes/day but 5 million reads/day, the design is clear: optimize for reads. I denormalize data where it makes sense. The leaderboard, for example, stores pre-computed rankings instead of calculating them on every page load. The daily challenge selection uses a simple ORDER BY id ASC LIMIT 1 query that's effectively free.
The scraper writes approximately 10 rows per hourly run (240 writes/day for challenges), plus user guess submissions. Even with many times today's traffic, writes would stay well under the 100K limit.
3. Durable Objects: The SQLite Trick
On the free tier, Durable Objects must use new_sqlite_classes instead of new_classes. I learned this the hard way after hours of "class not found" errors. The SQLite backing means DO state persists automatically - no need for explicit state saving, which actually simplifies the code.
Each game room DO handles its own WebSocket connections, game logic, and state. When the last player disconnects, the DO eventually gets evicted from memory, but the state remains in SQLite for if anyone reconnects.
4. Service Bindings Instead of HTTP
Since the Astro app and the DO Worker are separate Workers, they need to communicate. Instead of making HTTP requests between them (which would count toward the request limit and add latency), I use Service Bindings. A Service Binding lets one Worker call another as if it were a local function, with zero network overhead.
What Happens When You Hit Limits
I've come close to limits twice:
- Worker requests during a traffic spike - When PriceGuess was shared on a popular Israeli tech forum, we hit 80,000 requests in a single day. The free tier is 100,000/day, so we had headroom, but it was a wake-up call. If we'd gone viral, the site would have returned 1015 errors (Worker Limit Exceeded) for the rest of the day.
- D1 reads during leaderboard abuse - Someone wrote a script that refreshed the leaderboard every second. Each refresh triggered a D1 read query. I added rate limiting at the Worker level (using a simple in-memory counter) to prevent this.
Trade-offs of the Free Tier
Running on free infrastructure isn't without compromises:
- No KV on free for heavy caching - Cloudflare KV is available on free but limited. I use in-memory caching within Worker instances instead, accepting that cache is per-isolate and not shared globally.
- No analytics beyond basic - Cloudflare's free analytics are basic. For deeper insights, I'd need to log to an external service, which would use Worker CPU time.
- Deploy order matters - Service bindings require the target Worker to exist. I must always deploy the DO Worker before the Astro app, or the deploy fails.
- Cold starts - Workers have minimal cold start times (typically under 5ms), but Durable Objects can take 50-100ms to spin up if they've been evicted. For real-time games, this means the first action after connecting might feel slightly slower.
When to Upgrade
I'll move to the $5/month Workers Paid plan when any of these happen:
- Consistent 50,000+ daily active users (request limits)
- Need for Queues or Email Workers (paid-only features)
- CPU time becomes a bottleneck for new features
- D1 storage exceeds 5GB (unlikely for our use case)
Until then, the free tier handles everything PriceGuess needs. The key insight is that edge computing platforms aren't just for enterprise - they're viable for indie developers building real products.
Final Thoughts
The best architecture is the one you don't have to pay for until you have users who can pay for it.
Cloudflare's free tier gave me the runway to build, iterate, and grow PriceGuess without financial pressure. The constraints forced better architectural decisions - simpler queries, lighter renders, smarter caching. If you're building a side project and debating between a VPS and serverless, give the edge a try. You might be surprised how far free goes.