A DDoS test for a website asks how many requests per second the edge can absorb. A DDoS test for an API asks a narrower and more useful question: which requests, and what each one actually costs to answer.
That difference is the whole subject. On a static site every request is roughly equal and roughly cheap, so an attacker needs volume, and your defense is a volume problem you can watch on a bandwidth graph. An API breaks that symmetry. Two requests that look identical on the wire, same size, same method, same source, can differ by three orders of magnitude in what they cost the backend to serve.
So the request rate that takes an API down is usually not the one your traffic graph is showing you. It is a small number of requests aimed at the one endpoint that spends the most work per call, sustained by clients that retry on their own, against a rate limit that was measured against the wrong thing.
Testing an API for DDoS resilience means finding those endpoints, driving them the way a real client fleet would, and measuring cost per request class rather than requests per second. This post is about what changes when the target speaks JSON instead of HTML, and what a test has to do differently to mean anything.
What DDoS testing for an API is
DDoS testing for an API is a controlled load test that measures how an API's endpoints, authentication paths, and rate limits behave under attack-shaped traffic, focusing on backend cost per request rather than raw request volume, because the requests that exhaust an API are rarely the ones that dominate its bandwidth.
The general discipline, scoping, authorization, blast-radius control, and the metrics a test produces, is the same as for any target and lives in the complete guide to DDoS testing. What follows assumes that groundwork and spends its words only on the parts an API changes.
Cost asymmetry: the request the graph does not show
Picture two requests hitting the same host. One is GET /pricing, a cached, static response served from memory. The other is GET /api/v1/orders?search=*&sort=custom_field&expand=line_items,customer,shipments. Both are a few hundred bytes on the wire. On a bandwidth graph they are indistinguishable, two thin lines of ingress.
Behind the second one, the server parses a query, hits a database with a filter that cannot use an index, deserializes a few hundred rows, expands three related objects with their own queries, and serializes the lot back to JSON. That single request can cost a thousand times the CPU, memory, and database time of the first.
This is why an API DDoS test cannot be a single rate ramp against the front door. The meaningful measurement is per endpoint class: send a controlled rate at each distinct kind of request and record what it costs to serve, in CPU, in database time, in downstream calls, not just whether it returned 200. The output you want is a cost map, ranking endpoints by backend work per request. The attacker is building the same map from the outside; the test is you building it first.
The auth and token endpoints are the expensive front door
The most expensive endpoint on most APIs is the one that exists to keep attackers out.
A login or token-issuance endpoint deliberately does slow work. A modern password check runs a memory-hard hash (bcrypt, scrypt, argon2) tuned to take tens of milliseconds of CPU on purpose, because a fast hash is a crackable hash. Token minting adds a signing operation, a database write, sometimes a call to an identity provider. This is correct security engineering, and it is also a pre-built amplification factor: an attacker sending cheap, malformed login attempts forces the server to spend that hardening budget on every single one.
The attempts do not even need valid usernames. If the endpoint hashes a supplied password before it confirms the account exists (a common and otherwise-sensible way to avoid leaking which accounts are real), then every garbage request pays the full hashing cost. A few hundred requests per second at a login endpoint, a rate that is invisible on a bandwidth graph, can saturate CPU while the rest of the site sits idle.
So auth endpoints get their own test, and the metric is CPU and tail latency under a modest request rate, not throughput. Password reset, signup, token refresh, and any endpoint that sends an email or SMS belong in the same group: each one triggers expensive or metered work per call, and each one is usually reachable without already being authenticated.
That last point is the seam that matters for rate limiting.
Rate limits: per-key and per-IP, and the endpoints that have neither
An API's rate limit is only as good as the thing it counts by, and the two common keys each have a blind spot.
Per-IP limiting is the default because it needs nothing from the client, and it fails wherever many users share an address. Behind carrier-grade NAT, a corporate proxy, or a cloud egress gateway, thousands of legitimate users look like one IP, so a limit low enough to stop an attacker also throttles real traffic; a limit high enough to spare real traffic does nothing to an attacker who simply spreads across a residential proxy pool. Per-IP is a coarse instrument aimed at a moving target.
Per-key limiting (per API key, per token, per account) is precise, and it only exists after the client has a key. The endpoints that hand out keys and tokens, login, signup, token issuance, the exact expensive endpoints from the previous section, are by definition reachable before the caller is authenticated. There is no per-key limit to apply to the request whose entire purpose is to obtain the key. So the strongest limit protects the cheap authenticated calls, and the weakest limit guards the expensive unauthenticated ones.
Calibrating the threshold itself, where to set the rate, how the window behaves, why a rule in observe-only mode stops nothing, is a discipline in its own right; it is the subject of the rate-based rule definition and of testing AWS WAF rate-based rules, and this post links to them rather than repeating them. The API-specific point is upstream of the threshold: confirm that a limit exists on every unauthenticated expensive endpoint at all, then test whether the key it counts by can be trivially rotated or spread. A per-key limit an attacker defeats by requesting more keys is a limit in name only.
Search, pagination, and the cheap request with the expensive answer
Beyond auth, the endpoints worth attacking are the ones where a small input controls a large amount of work.
Deep pagination is the classic. ?page=1 and ?page=50000 are the same size and the same shape, but on an offset-based query the second forces the database to count through fifty thousand pages of rows to discard all but the last. Wildcard and unanchored search (search=%a%) defeats the index the search was built on and falls back to a scan. Faceted filters that combine several unindexed fields do the same. On a GraphQL endpoint the input control is even more direct: query depth, field aliasing, and fragment expansion let one request ask for a nested, multiplied result set that maps to hundreds of resolver calls.
None of these is a flood. Each is a single well-formed request that a legitimate client could plausibly send, which is exactly why they slip past volume-based defenses and why they belong in the test. Enumerate the endpoints that accept a search term, a sort field, a page offset, a date range, or a nested query, and for each one send the worst-case-but-valid input at a modest rate. The finding is the cost multiplier: how much more one adversarial-but-legal request costs than the median request to the same endpoint.
Retry storms: your own clients are part of the attack
The feature that makes an API DDoS test different from a website test one more way is that the client is programmable, and its programming can turn a small failure into a large one.
When an API starts returning errors, well-behaved clients retry. Badly configured clients retry immediately, then immediately again, with no backoff and no jitter. The moment the backend slows down, every client in the fleet converts one failed request into three or five, and the offered load climbs at the exact moment the server has the least capacity to serve it. The clients are not malicious; their retry logic is the amplifier. A brief dip becomes a self-sustaining flood that outlives its own trigger.
The related failure is the thundering herd. An API that was down comes back, and every client that was waiting reconnects in the same instant. Mobile fleets make this sharp: a push notification, a scheduled sync, or a recovery after a network blip synchronizes millions of clients onto the same second. The backend survived the outage and then falls over on the recovery.
This is a load profile a naive test never generates, because a test harness with clean, rate-limited, well-behaved virtual users is the one client fleet an API will never actually face. The test has to model the retry behavior of the real clients: no backoff, synchronized recovery, a stampede after a simulated blip. If the real clients honor a Retry-After header, test what happens when they do not, because some fraction never will.
What a 429 should do versus what it does
The correct response to too many requests is a 429 Too Many Requests, returned cheaply, early, and with a Retry-After header that tells the client when to come back. That is the design. Three things go wrong with it in practice, and each is a specific test.
First, the 429 is computed too late. If the request passes through authentication, hits the database, and only then trips a limit that returns 429, the server paid the full cost of a request it rejected. The status code says "shed," the work says "served." Rejection has to happen before the expensive path, at the edge or in a cheap early middleware, or it is not shedding load, it is doing the work and then apologizing.
Second, clients ignore Retry-After. A 429 without an honored backoff is just another error to retry immediately, which folds this straight into the retry-storm problem above. Test whether the limiter's own signal actually changes client behavior, or whether it is decoration.
Third, the limiter is the bottleneck. A rate limiter backed by a single shared counter (one Redis key, one row) becomes a contention point of its own under load: every request, including the ones about to be rejected, has to touch it. Now the mechanism meant to protect the backend is the layer that fails first. Drive the limiter hard enough to find out whether it sheds load or becomes load.
Running the test without becoming the outage
Everything above is an attack surface, so the test that maps it carries the same containment discipline as any other: an explicit scope, a rate ceiling per endpoint, live instrumentation, and a kill switch you fired this session. The mechanics of keeping a test inside its limits are covered in running a DDoS test without disrupting production, and the decisions that go into the plan and the confirmations you walk before launch are the test plan and the testing checklist.
Two containment points are API-specific and worth stating. Some of the most expensive endpoints have irreversible side effects: signup creates accounts, password reset sends email, an order endpoint charges a card. Testing those at rate means either a true staging replica of the downstream or a carefully stubbed side effect, never live production side effects driven at attack volume. And the downstream cost is often somebody else's: an endpoint that calls a metered third-party API turns your load test into their bill and possibly their rate-limit ban. That collateral reach, and the economic denial-of-service angle where sustained load converts into runaway spend rather than downtime, is both a finding and a reason to bound the test tightly.
FAQ
What is DDoS testing for an API?
It is a controlled load test that measures how an API's endpoints, authentication paths, and rate limits hold up under attack-shaped traffic, with the focus on backend cost per request rather than raw request volume. The requests that exhaust an API are usually a few expensive calls sustained by client retries, not a high-bandwidth flood, so the test measures cost per endpoint class and models real client behavior.
How is testing an API different from testing a website?
A website serves mostly cheap, roughly equal requests, so load is a volume problem visible on a bandwidth graph. An API serves requests whose backend cost varies by orders of magnitude, so a small number of calls to an expensive endpoint (a deep search, a login hashing a password) can saturate it at a rate that looks idle on a traffic graph. The test targets cost, not throughput.
Which API endpoints are most at risk?
Unauthenticated endpoints that do expensive work: login and token issuance (password hashing, token minting), signup and password reset (which send email or SMS), and search or pagination endpoints where a small input controls a large query. These combine high per-request cost with the fact that a per-key rate limit cannot apply before the caller has a key.
What is a retry storm and why does it matter for API testing?
A retry storm is when clients respond to errors by retrying immediately without backoff, multiplying the offered load at the moment the backend is weakest, so a brief failure becomes a self-sustaining flood. A related pattern, the thundering herd, is many clients reconnecting in the same instant after an outage. A realistic API test has to model this client behavior, because a harness of well-behaved virtual users never generates it.
Does returning a 429 protect an API?
Only if the rejection is cheap, early, and honored. A 429 computed after the request has already hit the database has already paid the cost it was meant to avoid; a Retry-After header that clients ignore does nothing; and a rate limiter backed by a single shared counter can itself become the bottleneck under load. Each of those is a distinct thing to test, not an assumption to make.
Capacity is a budget, not a number
The instinct carried over from website load testing is to ask for a single number: requests per second before it breaks. For an API that number is close to meaningless, because it depends entirely on which requests. The same server might absorb fifty thousand cached reads a second and fall over at three hundred logins, and both are true at once.
An API's real capacity is a budget spent unevenly across endpoints, and an attacker's whole job is to find the line item that costs the most to serve and the least to send. The auth path that hashes every guess, the search that scans every row, the client fleet that retries every failure: none of them show up on the graph you were watching. They show up on the cost map, and the only way to have the cost map before the attacker does is to build it yourself.
