Protecting a home-served API from a flood without spending a cent
ContenidoContents
I have a few tools on the site that, besides running in the browser, expose a free public API: generate Spanish fake data, validate an ID number, build an IBAN, generate UUIDs… All of that is served by a Mac mini at my home, behind a Cloudflare tunnel. It works great, but there’s one question that makes you a little dizzy: what if someone starts hammering one of those APIs?
This week I added the UUID API and took that question seriously. The answer has an interesting twist: the danger wasn’t where I thought it was.
The wrong fear
My first instinct was to look at the cost of generating the data. But generating a UUID, or an ID number, or an IBAN, is dirt cheap: a few random bytes and some arithmetic. Even at thousands of requests per second, the CPU barely notices. That wasn’t it.
The real problem was something far dumber: the telemetry.
The actual bottleneck
Every call to one of my APIs —and every time a browser tool records that it was used— writes two files: a usage counter and a fine-grained log for the analytics dashboard. And that write uses an exclusive, blocking lock (flock with LOCK_EX) on a file shared by all the tools.
Combine that with how Apache is set up: prefork, with a maximum of 250 processes serving requests. The recipe for disaster goes like this:
- A flood hits the API.
- Every request tries to write the same file and queues up waiting for the lock.
- Each queued request holds an Apache process.
- If all 250 fill up… Apache stops serving everything: not just the API, but the website, the blog, the Moodle classroom. Everything that machine serves.
In other words: a flood against /api/uuid/ could take down my entire server — not because of generating UUIDs, but because of waiting to write down that it had generated them. Absurd, but real.
Layer 1: the origin must never block
The first defense is plain common sense: telemetry doesn’t deserve to block anyone. I switched the lock to non-blocking (LOCK_EX | LOCK_NB):
1if ($fh && flock($fh, LOCK_EX | LOCK_NB)) {
2 // ... update the counter ...
3 flock($fh, LOCK_UN); fclose($fh);
4} elseif ($fh) {
5 fclose($fh); // busy: skip the record and move on
6}
The idea: if the file is busy, I don’t wait, I skip it. Under an extreme spike I’ll lose a count or two in the analytics, but no Apache process gets stuck on the lock. Better to lose a number than to take down the machine.
That closes the worst case at the origin. But ideally the flood never even reaches the Mac.
Layer 2: stop the flood at the edge (for free)
This is where Cloudflare comes in. And here’s the part that’s hard to find well explained: free rate-limiting exists, but not where you’d expect.
- If you go to Security → account-level WAF, it asks you to buy an add-on. That’s Enterprise, paid. Not there.
- Free rate-limiting lives inside each domain: open the domain → Security → Security rules → Rate limiting rules. The Free plan gives you one rule.
I set that rule to cover all my APIs and the telemetry endpoint:
starts_with(http.request.uri.path, "/api/") or http.request.uri.path eq "/track.php"
With these limits (on Free, period and duration only allow 10 seconds):
- Count by: IP
- Threshold: 15 requests per 10 seconds
- Action: block for 10 seconds
For a human it’s unreachable —my own APIs already cap at 300 requests/hour per IP— but for a script firing hundreds per second it’s an instant wall. And best of all: the blocking happens on Cloudflare’s servers, so those requests never touch my Mac.
Testing it
The theory is nice, but you need to see a 429 to your face. A burst of 25 requests in a row:
1for i in $(seq 1 25); do
2 curl -s -o /dev/null -w "%{http_code} " "https://sergiocomeron.com/api/uuid/?v=4"
3done
Result:
200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 429 429 429 429 429 ...
The first 15 go through; from the 16th onward, Cloudflare cuts them off with 429 Too Many Requests. After 10 seconds, normal use returns 200 again. Exactly what I wanted.
The result
Two layers, zero euros:
- Edge (Cloudflare): stops the flood before it reaches home.
- Origin (Apache/PHP): even if something slips through, it can’t block the processes.
And normal use doesn’t notice a thing. I sleep better knowing that one of my tools, no matter how hard someone tries to blow it up, won’t take the rest of the server down with it. Which, with every service living on a single machine, is exactly what I needed.