All articles Integration

Rate Limiting by Device, Not IP

Rate limiting is the quiet workhorse of abuse prevention, and most of it is anchored to the wrong identifier. The IP address was a reasonable proxy for “a user” two decades ago. Today it is a poor one. Carrier-grade NAT puts thousands of mobile users behind a single address, while a determined attacker rotates through a proxy pool and never trips a per-IP counter. The result is a control that frustrates legitimate users and waves through the abuse it was meant to stop.

Rate limiting by device changes the anchor from the network to the client environment. Instead of asking how many requests came from this address, you ask how many came from this device. That question is far more aligned with the behavior you actually want to bound.

Why the IP anchor fails

The IP address conflates two things that have drifted apart: the network path and the actor. Both failure modes hurt.

  • Over-blocking shared IPs. A university, an office, or a mobile carrier presents one address for a huge population. A per-IP limit tuned to stop one abuser throttles an entire building of legitimate users.
  • Under-blocking rotators. Residential proxy networks and VPN pools hand an attacker a fresh IP per request. Any per-IP counter resets constantly, so the limit never bites.
  • IPv6 amplifies both. A single user can hold an enormous address range, making per-address limits meaningless without prefix aggregation.

The core problem is that the IP is neither stable per user nor unique per user. Rate limiting needs an identifier with both properties, and the device fingerprint gets much closer.

What “by device” means

Device-based rate limiting keys the counter on a stable visitor ID derived from the browser or app environment rather than on the network address. That ID persists across IP changes and differs between users behind the same IP, which is exactly the pair of properties IP lacks.

Two refinements make it robust:

  • Weight by confidence. A device ID comes with a confidence score. Enforce strict limits on high-confidence IDs and fall back to coarser controls when confidence is low, so you never hard-block on a shaky match.
  • Layer, do not replace. Keep a generous IP-prefix limit as a cheap outer ring and apply the precise device limit inside it.
AnchorSame user, new IPMany users, one IPCost to evade
IP addressNew bucket, limit resetsOne bucket, all throttledLow
Device IDSame bucket, limit holdsSeparate buckets, fairHigh

That table is the whole argument in miniature. The device anchor holds the line when the IP changes and stays fair when the IP is shared.

Implementing it

The mechanics are straightforward once you have a visitor ID on the request. Verify the ID server-side, then use it as the rate-limit key.

// Express-style middleware sketch
async function deviceRateLimit(req, res, next) {
  const { visitorId, confidence } = await verifyPrynt(req);

  // Low-confidence IDs fall back to an IP-prefix bucket
  const key = confidence >= 0.8
    ? `dev:${visitorId}`
    : `ip:${ipPrefix(req.ip)}`;

  const count = await redis.incr(key);
  if (count === 1) await redis.expire(key, 60);

  const limit = key.startsWith("dev:") ? 30 : 120;
  if (count > limit) {
    return res.status(429).json({ error: "rate_limited" });
  }
  next();
}

Points worth noting:

  • Verify server-side. Never trust a device ID asserted by the client without server verification, covered in node server-side verification. Otherwise attackers simply forge distinct IDs.
  • Choose keys per endpoint. A login endpoint deserves a tight device limit; a public catalog page can be looser.
  • Set sane TTLs. Short windows for bursty actions, longer windows for things like signup abuse.
  • Pick the right response. A 429 is fine for APIs; for interactive flows a proof-of-work challenge taxes automation more gracefully than an error.

Handling evasion and edge cases

Device-based limiting is not magic, and pretending otherwise leads to blind spots. Sophisticated attackers use antidetect browsers to churn fingerprints, aiming to look like many devices.

  • Confidence catches churn. Randomized environments tend to produce unstable, low-confidence IDs. Route those to stricter fallback handling rather than granting each a fresh full quota.
  • Cross-reference signals. A flood of distinct device IDs sharing one datacenter IP and identical behavior is a device farm, not many users. Correlate to catch it.
  • Protect the privacy-conscious. Some real users run hardened browsers and will land in the low-confidence path. Give that path a reasonable limit, not a punishing one.
  • Watch for shared kiosks. Public terminals legitimately serve many people from one device. Flag these paths so a stable ID does not throttle sequential real users.

Because Prynt is self-hosted, the visitor ID, confidence score, and network signals are all available on your own infrastructure, so you can compose these limits without shipping request metadata to a third party. Pair this with rate-limiting-aware login protection for the highest-value endpoints, and see the SDK docs for retrieving the ID.

Frequently asked questions

Why is IP-based rate limiting unreliable?

One IP can hide thousands of users behind a NAT or a single attacker rotating through a proxy pool. So IP limits punish shared networks while barely slowing an attacker who changes address at will.

Can attackers evade device-based rate limits?

They can try, using antidetect browsers or randomized environments, but that raises their cost significantly. Combining the device ID with a confidence score and network signals closes most of the gaps.

Should I replace IP limits entirely?

No. Use both. IP limits are a cheap coarse filter, and device limits add precision. Layering them gives you defense in depth without discarding a useful first line.

Anchoring rate limits to the device instead of the IP fixes the two failures that make IP limits frustrating: it stops punishing everyone behind a shared address and stops letting rotators slip through. Weight by confidence, keep IP limits as an outer ring, and verify identity on the server. The result is throttling that finally bounds the thing you meant to bound, which is the actor, not the network path.

Run it yourself

Prynt is open-source, self-hostable device intelligence — visitor IDs, bot & fraud Smart Signals, and behavioral biometrics you own end to end.

Keep reading