People do not believe the fifty dollars. A million users sounds like a number that needs a platform team and a cloud bill in the thousands. Boringrats ran on donations. I set the infrastructure budget at $50 a month, and most months the bill came in closer to $35.
Boringrats was a place for small open source developers to find collaborators, find work, and get advice from people who had actually shipped something. It reached a million users. At peak, around 200,000 people were active on a normal day. Two of us ran it. My co-founder handled community, fundraising, and the public face. I owned the backend and the data layer.
After a year of it I could tell you where every request went and why the origin rarely had to care. This is the architecture that held a million users on a bill under fifty dollars, the numbers from the busiest day I measured, and the parts that nearly came apart.
What the bill actually looked like
The bill stayed small because most of what ran had a free tier or a usage-based price. A normal month looked roughly like this.
| Line | What it was | Cost |
|---|---|---|
| Origin VPS (Singapore) | one small box for the API and the workers | about $15 / month |
| PostgreSQL | managed, Hobbyist plan | from $12 / month ($0.02 / hour) |
| Redis | Upstash, pay as you go | about $3 / month ($0.20 per 100K commands) |
| Kafka | Aiven free tier | $0 |
| Cloudflare | subscription | $5 / month |
| Total | about $35 / month |
Cloudflare did the expensive part: TLS, the WAF, and the cache that answered 78.36 percent of requests. The origin was one small Singapore VPS. Postgres, Redis, and Kafka were managed, so I was not the one patching a database at 2am. Airflow ran on the VPS and kicked off the scheduled jobs.
Managed services cost more per unit than running the same software myself. I paid for them anyway because a two-person project has no on-call rotation, and automated backups on a managed database are cheaper than a week of downtime. The hours I saved went into the product and the pipelines.
The edge does the work
The most important number on the entire dashboard was the cache hit rate. Here is one 24-hour window from September 2025, during the busiest stretch we ever had:

2.01 million requests and 685,420 visits in a single day, with 9.83 GB of bandwidth served. The busiest hour hit 102,341 requests at 21:45. The United States sent the most traffic at 428,300 requests, the homepage carried 30.4 percent of the total, and 46.3 percent of all requests were for HTML.
78.36 percent of those requests never reached the origin. Cloudflare answered them from cache close to the user, and the VPS only saw the remaining fifth. On the day Boringrats handled more traffic than it ever had, the origin was mostly idle.
{
"type": "doughnut",
"data": {
"labels": ["Served from Cloudflare cache", "Reached the origin"],
"datasets": [{
"data": [78.36, 21.64],
"backgroundColor": ["#89b4fa", "#45475a"],
"borderWidth": 0
}]
},
"options": {
"responsive": true,
"plugins": { "legend": { "position": "bottom" } }
}
}
Every percentage point of cache hit rate is an entire class of work the origin never has to do. Pushing that number higher was worth more than any instance upgrade I could have bought.
The request path
flowchart LR
U[Visitors] -->|HTTPS| CF{Cloudflare edge}
CF -->|cache hit 78.36 percent| U
CF -->|cache miss| O[Origin VPS]
O --> DB[(Postgres)]
O --> R[(Redis hot keys)]
O --> S[(Object storage)]
A request lands at Cloudflare first. DNS and TLS terminate there, the WAF drops the obvious junk, and the cache decides whether this response already exists. Most of the time it does, and the visitor gets bytes from a data center near them. When it does not, Cloudflare goes to the origin, the origin builds the response from Postgres or Redis, and the edge keeps a copy so the next thousand people do not repeat the work.
The architecture
<svg viewBox="0 0 900 430" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Boringrats request path and data plane">
<defs>
<marker id="arw" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" opacity="0.7"/>
</marker>
</defs>
<g font-family="ui-monospace, SFMono-Regular, Menlo, monospace" fill="currentColor">
<text x="10" y="24" font-size="13" font-weight="700" opacity="0.6">REQUEST PATH</text>
<rect x="20" y="60" width="150" height="64" rx="8" fill="none" stroke="currentColor" stroke-opacity="0.45"/>
<text x="95" y="88" font-size="14" text-anchor="middle" font-weight="700">Visitors</text>
<text x="95" y="106" font-size="11" text-anchor="middle" opacity="0.6">1M users</text>
<rect x="250" y="46" width="250" height="92" rx="8" fill="none" stroke="currentColor" stroke-opacity="0.7" stroke-width="1.5"/>
<text x="375" y="76" font-size="14" text-anchor="middle" font-weight="700">Cloudflare edge</text>
<text x="375" y="98" font-size="11" text-anchor="middle" opacity="0.65">DNS / TLS / WAF</text>
<text x="375" y="116" font-size="11" text-anchor="middle" opacity="0.65">CDN cache 78.36 percent</text>
<rect x="580" y="60" width="180" height="64" rx="8" fill="none" stroke="currentColor" stroke-opacity="0.45"/>
<text x="670" y="88" font-size="14" text-anchor="middle" font-weight="700">Origin VPS</text>
<text x="670" y="106" font-size="11" text-anchor="middle" opacity="0.6">Docker: API + workers</text>
<line x1="170" y1="80" x2="248" y2="80" stroke="currentColor" stroke-opacity="0.7" marker-end="url(#arw)"/>
<line x1="250" y1="112" x2="172" y2="112" stroke="currentColor" stroke-opacity="0.7" marker-end="url(#arw)"/>
<text x="210" y="132" font-size="10" text-anchor="middle" opacity="0.6">hit</text>
<line x1="500" y1="92" x2="578" y2="92" stroke="currentColor" stroke-opacity="0.7" marker-end="url(#arw)"/>
<text x="539" y="82" font-size="10" text-anchor="middle" opacity="0.6">miss 21.64 percent</text>
<rect x="470" y="180" width="120" height="44" rx="8" fill="none" stroke="currentColor" stroke-opacity="0.45"/>
<text x="530" y="207" font-size="12" text-anchor="middle">Postgres</text>
<rect x="610" y="180" width="110" height="44" rx="8" fill="none" stroke="currentColor" stroke-opacity="0.45"/>
<text x="665" y="207" font-size="12" text-anchor="middle">Redis</text>
<rect x="740" y="180" width="140" height="44" rx="8" fill="none" stroke="currentColor" stroke-opacity="0.45"/>
<text x="810" y="207" font-size="12" text-anchor="middle">Object storage</text>
<line x1="620" y1="124" x2="560" y2="178" stroke="currentColor" stroke-opacity="0.5" marker-end="url(#arw)"/>
<line x1="660" y1="124" x2="665" y2="178" stroke="currentColor" stroke-opacity="0.5" marker-end="url(#arw)"/>
<line x1="700" y1="124" x2="780" y2="178" stroke="currentColor" stroke-opacity="0.5" marker-end="url(#arw)"/>
<line x1="10" y1="262" x2="890" y2="262" stroke="currentColor" stroke-opacity="0.25" stroke-dasharray="5 6"/>
<text x="10" y="290" font-size="13" font-weight="700" opacity="0.6">DATA PLANE</text>
<rect x="20" y="310" width="130" height="54" rx="8" fill="none" stroke="currentColor" stroke-opacity="0.45"/>
<text x="85" y="342" font-size="13" text-anchor="middle">Kafka</text>
<rect x="220" y="310" width="160" height="54" rx="8" fill="none" stroke="currentColor" stroke-opacity="0.45"/>
<text x="300" y="334" font-size="13" text-anchor="middle">Ingestion workers</text>
<text x="300" y="351" font-size="10" text-anchor="middle" opacity="0.6">dedupe + rollups</text>
<rect x="450" y="310" width="160" height="54" rx="8" fill="none" stroke="currentColor" stroke-opacity="0.45"/>
<text x="530" y="342" font-size="13" text-anchor="middle">Warehouse</text>
<rect x="680" y="310" width="150" height="54" rx="8" fill="none" stroke="currentColor" stroke-opacity="0.45"/>
<text x="755" y="342" font-size="13" text-anchor="middle">Airflow</text>
<line x1="150" y1="337" x2="218" y2="337" stroke="currentColor" stroke-opacity="0.7" marker-end="url(#arw)"/>
<line x1="380" y1="337" x2="448" y2="337" stroke="currentColor" stroke-opacity="0.7" marker-end="url(#arw)"/>
<line x1="680" y1="322" x2="382" y2="322" stroke="currentColor" stroke-opacity="0.4" stroke-dasharray="4 5" marker-end="url(#arw)"/>
<line x1="680" y1="350" x2="612" y2="350" stroke="currentColor" stroke-opacity="0.4" stroke-dasharray="4 5" marker-end="url(#arw)"/>
</g>
</svg>
The rules that kept the bill flat
Cache before you scale. When a page is slow, check whether the response should have already existed before you resize anything. Most content on a read-heavy site is identical for everyone, and identical work is waste.
Keep the origin replaceable. Nothing important lived only on that VPS. The database, the object storage, and the repositories were elsewhere. If the box died I could rebuild it from a script, which meant I never paid for a second one "just in case".
Strip and compress. HTML went out compressed, images were sized before they were uploaded, and the payloads that reached a phone were as small as I could make them. Smaller payloads kept the bandwidth line low and the pages fast.
Queue the writes. A comment or a reaction did not have to finish inside the request. Push it to a queue, return quickly, let a worker do the slow part. The visitor waits for almost nothing, and the origin survives a spike.
Back up, then prove the backup. Dumps and media went to object storage on a schedule, and I restored them into a scratch database often enough to trust them.
Watch the tail. The average was always calm. The p95 told me when something was actually wrong, and it usually showed up before a user complained.
What the bill did not cover
The invoice was the small part. The rest was time and sleep, and it eventually ran out. A traffic spike broke a pipeline I had built six months earlier as a stopgap, and it broke during my final year, with research deadlines and exams already on the calendar. Both of us were unpaid. The platform was sold in November 2025 and shut down afterwards. I wrote about that separately in the acquisition post, because the numbers here only tell half of it.
What I would tell you
If you are building something that people keep showing up to, the cheap architecture is usually the correct one. Cache at the edge, keep the stateful parts boring and backed up, and treat every service you add as a future page at 2am. The expensive resource on a project like this is attention, and the architecture should send it to the product instead of the plumbing.
The budget was fifty dollars a month and the bill came in under it, mostly because the expensive work happened at the edge. I would keep most of that design even without the constraint.
Reach me at swadhinbiswas.cse@gmail.com or on GitHub and LinkedIn.
No comments yet.