What arrives once you are public
Within hours of a public IP existing, it is being scanned. Within days of a domain resolving, forms are being submitted by things that are not people. The mix is consistent:
- Scanners probing for
/wp-admin,/.env,/.git/configand a few thousand other paths./.envis the one that matters: an exposed environment file is a complete compromise, and it is checked because it works. - Credential stuffing: lists of leaked email and password pairs from other breaches, tried against your login form.
- Signup spam, to use your service, your storage, or your outbound email as a relay.
- Scrapers and crawlers, including a large and growing volume of AI training crawlers, some of which ignore
robots.txtentirely. - Somebody’s misconfigured client retrying forty times a second because their first request failed.
None of this is targeted. You do not need to be interesting.
Token bucket, and why fixed windows leak
A fixed window (100 requests per minute) allows 200 requests across a two-second boundary: 100 at 11:59:59 and 100 at 12:00:00. For a limit protecting an expensive resource, that doubling matters.
A token bucket holds N tokens, refilled at a steady rate. Each request takes one. It permits a burst up to the bucket size and then enforces the long-run rate. It is what you want, it is a few lines of code, and it is what this project’s rate limiter uses.
Per-IP is not enough on its own
Layer three limits, because each one fails differently:
- Per IP: catches crude floods. But carrier-grade NAT means an entire city can share an address, a university looks like one client, and behind a CDN every request appears to come from the CDN unless you read the forwarded header correctly. Keep this limit loose, or you will block real people in blocks.
- Per account: the accurate one for anything a signed-in user does. This is where the real limits belong.
- Per endpoint: because
/loginand/searchand/api/generate-imagehave nothing in common. One should tolerate thousands of requests; one should tolerate five failures in a row.
And be exact about the expensive endpoints, because rate limits protect the bill as much as the box. Anything that calls a paid API, sends an email or SMS, generates an image, or runs a model needs a hard per-account daily cap, not just a rate. That is what this project’s ai_usage table does: ten AI chats per person per day during the beta, claimed last and refunded if the provider fails.
Answer with 429, and say when
HTTP/1.1 429 Too Many Requests
Retry-After: 30A 429 with no Retry-After teaches every well-behaved client to retry immediately, which is the opposite of what you asked for. Include it.
Do not use 403 for rate limiting. A client needs to distinguish “you are going too fast” from “you are not allowed”, and so does the person reading your logs at 2am.
Fail open or fail closed?
When the limiter itself is unavailable (the store is down, the timeout fires) you must choose in advance.
The reasonable default is to fail open for authenticated, trusted users and fail closed for anonymous writes. Losing the limiter should not be an outage for the people you know; it also should not be an open door for the people you do not. This is the same principle the moderation pipeline in this project uses when a classifier tier is degraded: keep trusted members working, hold the untrusted.
Whatever you choose, write it down. The default is usually “fail open” because nobody decided, and that is worth knowing.
Signup abuse specifically
Email verification before anything expensive. Cloudflare Turnstile is free and does not make users identify traffic lights; hCaptcha has a free tier. Block disposable email domains if it fits your product, and know that the lists are never complete.
One specific warning, because it costs real money: do not put SMS OTP on an open signup form. SMS pumping (fraudulent traffic to premium-rate numbers, where the attacker takes a share of the carrier revenue) has cost companies seven-figure sums. If you need SMS, rate-limit it per number and per IP, restrict country codes to those you serve, and cap the daily spend at the provider.
The free path
Cloudflare’s free plan gives you basic WAF rules, bot-fight mode and a small number of rate-limiting rules at the edge, which stops the traffic before it reaches your bill. nginx’s limit_req is free. fail2ban on a VM reads your logs and blocks repeat offenders at the firewall.
And put /.env, /.git and /wp-admin in a deny rule. They will never be legitimate, and blocking them at the edge removes a large share of your log noise on day one.
Every lesson here is open. Your progress is recorded once you pass the Agents and Automation exam.
Check your understanding
You add a rate limit of 100 requests per minute per IP address. Complaints arrive from users in one city who are being blocked while browsing normally, and separately your login endpoint is still being brute-forced. What is going on?
Pick the one you would defend. Your answer stays private.
Stuck on something?Ask Tri, your AI tutor