An API is a DDoS target with its defenses removed
Most Layer 7 mitigation leans on one convenient fact: real website visitors arrive in browsers. A lightweight challenge asks the client to run a little JavaScript and hand back a signed cookie, and anything that cannot do that was never a customer.
APIs have no such tell. curl, an SDK, a mobile app, a CI pipeline and a payment webhook are all legitimate clients, and not one of them can solve a browser challenge or keep a cookie jar. Point browser-shaped protection at /api/ and you get the worst of both worlds: honest integrations break the moment protection tightens, while an attacker who simply ignores the redirect keeps hammering the endpoint.
That is the whole problem in one sentence. API mitigation cannot ask "is this a human?" — it has to ask "is this client identified, and is it staying inside its budget?"
What changes between a website and an API
| Website traffic | API traffic | |
|---|---|---|
| Legitimate client | A browser | curl, SDK, mobile app, webhook, CI job |
| Can solve a challenge | Yes, invisibly | No, ever |
| Stores cookies | Yes | Usually not |
| Identity signal | Browser verification, navigation rhythm | API key, bearer token, signed webhook header |
| Expected error format | An HTML page | JSON body with a correct status code |
| Cheapest defense | Edge cache plus challenge | Per-client rate limits plus cost caps |
How API DDoS attacks actually look
- Flat endpoint floods — thousands of requests per second at a single route, spread across rotating addresses, at a rate no real client would ever produce.
- Expensive-query abuse — deep pagination (
?page=48000), an unboundedlimit, wide date ranges or sort-by-everything parameters that turn one request into a full table scan. - GraphQL query bombs — one syntactically valid query with nested relations that expands into thousands of resolver calls. Tiny request, enormous cost.
- Token grinding and credential stuffing — automated attempts against
/auth/loginor/oauth/tokenusing leaked password lists. An account-takeover attempt and a flood on your most expensive endpoint at the same time. - Webhook and callback floods — your public callback URL is documented by definition, and replaying captured payloads is trivial if you do not verify signatures.
- Cache-busting parameters — randomized query strings crafted so every request misses cache and lands on your origin.
- Accidental self-DDoS — one client shipping a retry loop with no backoff. Not malicious, indistinguishable from an attack, and the fix is the same.
The common thread is the cost asymmetry behind every Layer 7 attack: the attacker spends one cheap HTTP request, you spend database time. API endpoints map directly onto expensive operations, which makes them the highest-leverage target on your infrastructure.
Step 1 — Identify the client before you limit it
A rate limit is only as good as the key it counts against, and the IP address is both the default choice and the weakest one: a botnet has thousands of addresses, while a corporate NAT or a mobile carrier puts thousands of your real users behind a single one.
Prefer, in order:
- An authentication credential — API key, bearer token or account ID. Hash it before using it as a limiter key, so the raw secret is never stored.
- A verified webhook signature — check the signature and limit the sender, not the address it came from.
- The IP address — the fallback for genuinely anonymous endpoints, and a good reason to keep those endpoints cheap.
Requiring a key on every non-public route is the highest-value change available to you. It converts anonymous flooding into an identified client exceeding a documented budget — a problem with an obvious, automatable answer.
Step 2 — Rate-limit in a way machines can obey
- Token bucket per client, so a legitimate burst is allowed and a sustained flood is not. Far friendlier to real SDK usage than a fixed window that resets on the second.
- A zone-wide ceiling on top of per-client limits, so ten thousand freshly registered keys cannot each behave "legally" and still take you down.
- Separate budgets per cost class.
GET /v1/statusandPOST /v1/reportsdo not belong on the same limit. - Answer with
429and aRetry-Afterheader, plusRateLimit-LimitandRateLimit-Remainingif you can. A well-behaved client backs off on its own; a response it cannot parse turns your mitigation into an outage. - Never send a challenge page, CAPTCHA or
302to an API path. A machine client reads that as a corrupt response, retries harder, and joins the flood you were trying to stop.
Step 3 — Make each request cheaper to serve
Rate limits bound how many requests arrive. Cost caps bound how much damage each one can do.
- Cap
limitandper_page, reject unbounded ranges, and use cursor pagination so deep offsets stop being expensive in the first place. - Enforce query depth and complexity limits on GraphQL, and disable introspection in production.
- Set hard timeouts on database queries and upstream calls, so one slow path cannot pile up connections until the pool is gone.
- Cache what is cacheable — public GETs, reference data, computed aggregates — with short TTLs at the edge. Every cache hit is a request your origin never serves, which is why a CDN is also attack armor for APIs.
- Move genuinely expensive work into a queue and return
202with a job URL, so a burst becomes a backlog instead of an outage.
Step 4 — Roll it out without breaking your clients
- Measure first. Pull p95 and p99 request rates per client from your logs. Real limits come from real percentiles, not round numbers.
- Run in log-only mode for a week or two and watch who would have been limited. It is usually one integration with a broken retry loop, not an attack.
- Set the limit above your noisiest honest client, then enforce it.
- Document the limits and the
429contract in your API reference, so integrators handle it correctly before they ever hit it. - Alert on limit hits, not only on downtime. A client crossing its budget is the earliest warning you get that something has changed.
Step 5 — Lock the origin down
None of the above matters if your API can be reached directly. Attackers look up the hostname's DNS history, find the server address, and skip your filtering edge entirely. Firewall the origin so it accepts traffic only from your mitigation network, and close the usual leaks — unproxied subdomains, mail records, certificate transparency logs. The full walkthrough is in how to hide your origin IP address.
How Itnetic protects API paths
Our default defense is a browser challenge, which is exactly the wrong instrument for machine traffic — so API paths get their own pipeline. You mark path prefixes such as /api/ or /graphql as an API zone, and matching requests are treated as machine traffic end to end: never challenged, never given tag cookies, and never counted by the browser-oriented flood detectors that would otherwise read a cookieless SDK as an attacker.
Protection inside the zone is exactly what this article argues for. Per-client token-bucket limits keyed by a header you choose — Authorization is the usual pick, hashed so the raw credential is never stored — with an IP fallback for anonymous routes, plus a zone-wide flood ceiling that arms a short attack window. Rate-limited clients get a 429 with Retry-After, never HTML. For the attack window you pick the posture: block flagged attacker fingerprints, additionally reject requests missing your key header, or log only while you tune. Invalid zone configuration fails open to the normal web pipeline, so a bad rule never takes your API down.
Every verdict lands in per-request logs alongside the client fingerprint and the rule that fired — which is what you need when an integrator asks why their nightly job started getting 429s. Attack traffic is never metered against your bandwidth quota, so being attacked does not produce an invoice; see pricing.
Itnetic's DDoS protection covers websites and APIs on the same edge network, enabled with two DNS records. If you want the background first, start with what DDoS mitigation is.