
How much one small server carries
A load test of signed-in users doing what the app does all day, the first capacity numbers, and why production numbers from India say more about distance than about the server.
14 posts.

A load test of signed-in users doing what the app does all day, the first capacity numbers, and why production numbers from India say more about distance than about the server.

Reloading any page in the app, or opening a shared link, landed on Today. The SPA rewrite rules pointed at /index.html, and Cloudflare Pages redirects that to /. It had been true for every deployment.

The API runs on a GCP e2-micro because that's what Always Free covers - one instance, 30 GB of standard disk, an ephemeral IP. It turns out to be sitting exactly at that ceiling, which explains more of the outage two posts back than any single bug did.

The API went down behind a Cloudflare Tunnel error and stayed down through a reboot, an interrupted OS upgrade, a stale connection pool, and a box too small for its own workload. None of the four were the same bug.

Three real bugs found by using the OTP flow the way a person actually would: a stuck login, a false 'already registered' error, and a confirmation screen that didn't lead anywhere. Plus a status page that finally watches the whole stack.

Login worked, /me failed, and the browser blamed CORS. The real cause was three layers down: Swarm silently ignoring a short-form tmpfs key, which meant Starlette's outermost error middleware - the one layer that runs before CORS headers exist - was the one answering.

app and api had no DNS records for most of their existence, so there was nothing to monitor - which meant status.pacestreak.com kept quietly not telling the whole story even after both went live. Fixed, plus a lesson in why a 200 from the app proves nothing about the API behind it.

The API's hosting decision is made: a free-tier GCP VM, Neon and Upstash, zero-downtime rolling updates on a single-node Swarm - and why the VM pulls its own updates instead of GitHub Actions pushing them.

Everything in the PaceStreak org is private except the status page and the org's .github repo. Here is why neither should be 'fixed'.

A status report: what is built across the API, the app and the sites, what's tested, and the one unresolved decision (where the API runs) that everything else is waiting on.

A proxied DNS record with nothing behind it returns 522, which looks like a broken product. So records wait for deployments.

The complete technology list behind PaceStreak — domain, sites, monitoring, backend, tooling — with the reasoning, the rejected alternatives, and the constraints each choice created for the next one.

Setting up the PaceStreak backend on uv, Ruff and ty, then wrapping it in Docker. Every real bug came from running it, not from reading it.

Domains, monitoring, a status page and a landing site — and the four things that silently broke while doing it.
PaceStreak is a habit and streak tracker for training and everything else worth doing regularly.