What is a DoS attack versus legitimate traffic?
A denial-of-service (DoS) attack is traffic whose purpose is to exhaust a resource so intended clients cannot use it. Legitimate high-volume traffic is the same resource consumption from customers, partners, or automation that the product is supposed to serve. Volume is a symptom. Intent and identity are the distinction.
Search crawlers, scrapers, scanners, attackers, and paying users can produce similar request rates. A badly behaved bot looks like a large customer. Blocking the customer can cost more than letting a short attack run, especially when availability is an SLA metric. Edge firewalls, coarse rate limits, and WAFs try to separate friend from foe, but they work from network and header signals. Those signals are easy to rotate, share, or spoof.
You distinguish the two by asking who is sending the traffic and whether that identity is allowed to consume this resource at this rate. That is an application question. It is also how you stop API abuse that returns 200 and still harms the product.
How do IP, user-agent, and fingerprinting compare?
Three common approaches try to decide whether a spike is an attack. The following table scores them on accuracy, false-positive risk, and evasion resistance:
| Approach | Accuracy | False-positive risk | Evasion resistance |
|---|---|---|---|
| IP volume and reputation | Low | High on CGN, offices, and VPNs | Low against rotating proxies |
| IP plus User-Agent | Low to medium | High when many users share one IP and one browser string | Low. Both fields are attacker-controlled |
| Application fingerprinting | High when keyed on a stable identity | Low for authenticated users with known plans | High. Rotation of IP or headers does not reset the budget |
Use IP and User-Agent as anonymous fallbacks, not as the only identity. Use application fingerprinting when you can name the user, session, API key, or tenant.
When does IP-based detection fail?
The cheapest DoS detector counts requests from one IP address. Reputation data can add signal: the ASN, whether the address is residential, cloud, or VPN, and whether that address already appears in abuse lists.
Attackers buy residential and mobile proxy pools and rotate the source address on every request. Each request looks like a new client, so a per-IP counter never fills. Real users also share addresses. Carrier-grade NAT puts many phones behind one public IP. A corporate egress or a popular VPN exit does the same. An IP-only limit then either blocks a whole office or misses a distributed flood.
Treat IP as a useful anonymous characteristic and a source of reputation, not as proof of a single human. Trust X-Forwarded-For and similar headers only when a known proxy wrote them.
Does adding the User-Agent header help?
The User-Agent header is how an HTTP client names itself. Combined with an IP, it can split clients that share a CGN or office egress: one phone browser and one curl process no longer share a single bucket.
The header is untrusted input. Anyone can send any string. An attacker who rotates both IP and User-Agent defeats the pair. A headless browser behind a residential proxy can send a current Chrome string and look like a shopper. User-Agent is a weak extra dimension, not a fingerprint you can defend.
How does application fingerprinting separate customers from attacks?
The application already knows more than the network path. A session token, user ID, hashed API key, or tenant ID names the client across IPs and devices. With that identifier you can ask:
- Is the caller authenticated or anonymous?
- Which plan and typical usage does this customer have?
- How long has the account existed?
- Is this request on a cheap read or an expensive export?
You then apply different limits, challenge suspicious sessions, or fail closed on anonymous abuse while leaving a known enterprise integration alone. That is fingerprinting with application context: you build a stable characteristic set and enforce policy on it.
Arcjet builds a SHA-256 fingerprint from the characteristics you configure. The default is ip.src. You can add built-in request fields or a custom key such as userId. Combining characteristics creates one fingerprint from the full set. Separate rules if you want independent IP and user budgets.
Built-in characteristics include ip.src, http.host, http.request.headers["<name>"], http.request.cookie["<name>"], http.request.uri.args["<name>"], and http.request.uri.path. Pass custom values into protect() as strings, numbers, or booleans. Do not use raw email addresses or other personal data when an opaque internal ID exists.
The following example keys Shield and a fixed window on the authenticated user, and falls back to IP for guests:
import arcjet, { fixedWindow, shield } from "@arcjet/node";
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [shield({ mode: "LIVE" })],});
const forUsers = aj.withRule( fixedWindow({ mode: "LIVE", characteristics: ["userId"], window: "60s", max: 100, }),);
const forGuests = aj.withRule( fixedWindow({ mode: "LIVE", characteristics: ["ip.src"], window: "60s", max: 20, }),);
export async function handler(req, user) { const decision = user ? await forUsers.protect(req, { userId: user.id }) : await forGuests.protect(req);
if (decision.isDenied()) { const status = decision.reason.isRateLimit() ? 429 : 403; return { status }; }}fixedWindow accepts window as a duration string such as "60s" or "1h", or as a number of seconds. shield inspects the request for common application-layer attack patterns. Both rules run in your process, so they see the session you already loaded.
What should the edge block, and what must the application decide?
Application-layer analysis has a hard limit: the request reaches your process. A volumetric flood can saturate the network or the origin before your handler runs. Network products exist to absorb that class of attack. Application rules exist to decide whether this identity may consume this resource.
Cloud providers document a free infrastructure baseline. AWS states that every customer receives AWS Shield Standard at no additional charge, and that Shield Standard defends against common network and transport layer (Layer 3 and 4) DDoS events. Amazon CloudFront, Amazon Route 53, and AWS Global Accelerator receive comprehensive availability protection against known infrastructure-layer attacks. That is not an application-layer identity control, and it is not a substitute for per-user quotas.
Vercel documents automatic DDoS mitigation for every deployment, on every plan, and that it blocks traffic it identifies as abnormal or suspicious. The platform-level firewall covers infrastructure and application-layer floods. It still cannot see your authenticated user, plan, or typical usage unless you enforce those rules in the application.
Keep the edge for saturation. Put identity-aware limits in the handler. Start anonymous limits low, raise them for authenticated users, and raise them again for known high-volume customers. If a request looks suspicious, re-authenticate or challenge that session instead of blocking the whole IP.
How should you respond to a traffic spike?
When volume jumps, do not flip a global IP ban first. Identify the characteristic that is growing: one user, one API key, one path, or many unrelated IPs. A single customer exporting data is a product event. Many rotating addresses hitting login is an attack. Mixed authenticated traffic at normal per-user rates is often a launch or a campaign.
Instrument the decision. Log the rule, the fingerprint characteristics, remaining capacity, and whether the caller was authenticated. Dry-run new limits against live traffic before you enforce them. Return 429 for quota exhaustion and 403 for attack denials so clients and monitors can tell them apart.
Application context is how you keep a large customer online while you clamp anonymous and abusive clients. IP and User-Agent cannot do that job alone.
Frequently asked questions
How do you tell a DoS attack from a traffic spike?
Volume alone cannot tell them apart. A DoS attack aims to exhaust a resource so intended clients cannot use it. A traffic spike from a large customer is the same consumption from an identity you intend to serve. Key limits on user, API key, or tenant, and compare the spike to that identity's typical usage and plan.
Why isn't IP-based rate limiting enough?
Many real users share one address behind carrier-grade NAT, offices, and VPNs, so an IP limit produces false positives. Attackers rotate residential and mobile proxies, so an IP limit also misses distributed floods. Use IP as an anonymous fallback, not as the only identifier.
Can a WAF or edge DDoS product distinguish a paying customer from an attack?
Edge products absorb saturation and common infrastructure-layer floods. They do not see your session, plan, or typical usage unless you forward that context. Keep volumetric protection at the edge and enforce identity-aware limits in the application.
What do AWS Shield Standard and Vercel DDoS mitigation cover?
AWS documents that Shield Standard is included for every customer and defends against common Layer 3 and 4 DDoS events, with stronger infrastructure-layer coverage on CloudFront, Route 53, and Global Accelerator. Vercel documents automatic DDoS mitigation for every deployment on every plan. Neither replaces per-user application quotas.
Application security in your code
Protect your application with Arcjet
Get rate limits, bot detection, and attack blocking in your request handlers.