Input and resource controls

Limit resource consumption

Bound request size, execution time, pagination, uploads, concurrency, and expensive downstream work before one caller can exhaust shared capacity.

A request can be perfectly valid and still too expensive to serve. Attackers look for operations where a small request creates a lot of work. So do badly written client retry loops, and in my experience those cause most real capacity incidents.

The defense is the same for both: put a limit on every dimension a caller can push.

Bound the obvious dimensions

Set limits at the edge and inside the application. Page size, query complexity, file size, processing time, retries, concurrent jobs.

Pagination is where most APIs leak capacity first:

curl -i "https://api.example.com/invoices?per_page=100000"
# HTTP/1.1 400 Bad Request
# {"error":"per_page must be between 1 and 100"}

You could also clamp silently to 100. Rejecting is my preference, because it makes the limit visible to client developers. Either way, the database never sees an unbounded query.

Rate limiting handles frequency. Return a controlled response, and keep an eye on who hits it:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit-Remaining: 0

A caller who hits the limit constantly is either broken or probing. Both deserve a look.

Price operations by their real cost

One global rate limit treats every request as equal. They are not. A 2 KB report request can trigger a minute-long database scan and a 200 MB export. A global limit misses that difference and may end up punishing cheap requests instead.

Measure the expensive unit directly. Rows scanned, worker seconds, memory, provider calls. Then give each operation its own budget:

GET  /invoices          600 requests/min per account
POST /reports           5 requests/min, max 2 concurrent per account
POST /invoices/import   1 running job per account

Per-operation budgets are explainable. A client developer understands “5 reports a minute”. And ordinary invoice reads keep working while the heavy stuff is throttled.

Verify the failure is controlled

Exceeding a limit must produce a clean rejection. Not a half-finished job that already burned the resources you were trying to protect.

Start two reports for one account, then try a third:

# third concurrent report for the same account
curl -i -X POST https://api.example.com/reports \
  -H "Authorization: Bearer $TOKEN"
# HTTP/1.1 429 Too Many Requests

Now check the worker queue. There should be no orphaned job for the rejected request. If there is, the limiter answered 429 after the work was already enqueued, and it protects nothing.

Try this on your heaviest endpoint: measure database time, response bytes, and queued work for one report request. Then exceed a page limit, a timeout, and a concurrency limit, one at a time. Prove the API fails predictably each time, and that a normal request still succeeds while it does.

Lesson completed