Skip to main content
Credential stuffing is the same stolen-password list tried against many accounts, usually from anonymized infrastructure and usually rotating IPs to stay under per-IP limits. ShieldLabs gives you two things an attacker cannot easily rotate away: the anonymity of each login session and the link between the many accounts one device or one local IP touches. Your code uses both to add friction on top of your own login rate limits.

Layer 1: anonymity at login

A per-request Risk Score on each login attempt. Datacenter, VPN, proxy, Tor, and anti-detect signals are common on stuffing traffic and arrive in real time on the webhook.

Layer 2: fan-out across accounts

Server-side Abuse Patterns that link one source to many accounts: “Many Accounts on One Device”, “Many Accounts on One Local IP”. Read on the dashboard.
ShieldLabs scores and links. Your login endpoint owns the action: allow, throttle, require a CAPTCHA or second factor, or reject. The point is not a single verdict but raising the cost of each attempt until the attack is no longer worth running.
Bring your own failed-attempt counters and login rate limits. ShieldLabs supplies what they cannot: how anonymous each session is, and which accounts a single device or local IP has touched. Combine the two, and keep the throttling logic in your backend.

Rate-limit on the DeviceID, not just the IP

The reason per-IP limits fail against stuffing is that attackers rotate IPs cheaply (proxy pools, residential proxies, a new exit per request). The DeviceID is harder to move: it is derived from dozens of stable browser components, so it stays the same across an IP rotation within the same browser. Keying your throttle on the DeviceID (alongside the IP and the account) makes rotation stop working.
Throttle by DeviceID across rotated IPs
app.post('/api/login', async (req, res) => {
  const { username, password, shieldRequestId } = req.body;

  const shield  = await waitForScore(shieldRequestId, 1500);
  const score   = shield?.Score ?? 0;          // 0-100
  const signals = new Set((shield?.Details ?? []).map((d) => d.Description));
  const deviceId = shield?.DeviceID;

  // Your own counter, keyed on the durable DeviceID (survives IP rotation).
  const attempts = await bumpAttemptCount(`dev:${deviceId}`); // 15 min window

  // 1. One device hammering many logins, even across rotated IPs and accounts.
  if (deviceId && attempts > YOUR_DEVICE_ATTEMPT_LIMIT) {
    return res.status(429).json({ action: 'rate_limited' });
  }

  // 2. Anonymity signals common on stuffing traffic raise the cost of a try.
  const anonymized = signals.has('Datacenter IP') || signals.has('Tor') ||
                     signals.has('Anti-detect Browser') || score >= 30;
  if (anonymized) {
    return res.status(200).json({ action: 'require_captcha' });
  }

  return continueLogin(username, password, res);
});
Combine keys for defense in depth: throttle on the DeviceID (survives IP rotation), on the local IP, and on the account being targeted. A stuffing run trips at least one even when it rotates the others.

Layer 1: score the login session

A login arriving from a datacenter range, a VPN, Tor, or an anti-detect browser is not proof of an attack, but it is exactly the profile stuffing traffic tends to carry. Read the Details and weigh the specific signals, not just the number.
function loginFriction(shield) {
  const fired = new Set((shield?.Details ?? []).map((d) => d.Description));
  if (fired.has('Tor') || fired.has('Anti-detect Browser')) return 'strong'; // CAPTCHA + 2FA
  if (fired.has('Datacenter IP') || fired.has('VPN') || fired.has('Proxy')) return 'medium'; // CAPTCHA
  if ((shield?.Score ?? 0) >= 60) return 'strong';
  return 'none';
}
See Acting on the Risk Score for the per-band playbook and Signals for what each signal means.
Honest framing: a legitimate user on a corporate VPN logs in every day. Anonymity raises friction (a CAPTCHA, a second factor), it does not justify a hard block on its own. Reserve outright rejection for the combination of anonymity, a high failed-attempt count, and a device or IP that is already fanning out across accounts.

Layer 2: catch the fan-out across accounts

The defining shape of stuffing is one source touching many accounts. ShieldLabs links sessions over time and grades each entity Normal → Suspicious → Dangerous as that count climbs.
One device attempting or reaching many different accounts. The grouping identity is the durable DeviceID, so it holds even as the attacker rotates IPs and clears cookies between attempts.
Many accounts reached through the same local IP. Catches a single machine or NAT fanning out across accounts behind a rotating public IP.
The same account whose VisitorID or DeviceID keeps changing, a hint of scripted attempts or anti-detect tooling cycling its environment between tries.
Pull the flagged entities from the dashboard Patterns tab (CSV or JSON) and feed the Dangerous DeviceIDs and local IPs into your login throttle as a watchlist. You can also reconstruct a device’s fan-out live from the History API.
How many accounts has this device touched?
curl "https://api.shieldlabs.ai/{domain}:{secret}/history/device_id/5eb7fd5c-1c5e-4a9f-9b21-7d2e8c0a1234?limit=100"
Escalate a known fan-out device
async function deviceFanOut(deviceId) {
  const rows = await shieldHistory('device_id', deviceId, 100);
  return new Set(rows.map((r) => r.UserHID).filter(Boolean)).size; // distinct accounts
}

if ((await deviceFanOut(deviceId)) >= YOUR_ACCOUNT_FANOUT_LIMIT) {
  // This device is reaching many accounts: require a second factor or block the device.
  return res.status(200).json({ action: 'step_up_2fa' });
}

Putting it together

1

Score every login

Call checkAuthenticatedUser (or checkAnonymous before the account is known) at the login step. See Snippet setup.
2

Throttle on the DeviceID

Key your failed-attempt counter on the DeviceID as well as the IP and the account, so rotating IPs no longer resets the limit.
3

Add friction on anonymity (Layer 1)

Require a CAPTCHA on datacenter, VPN, or proxy logins; require a second factor on Tor or anti-detect, or on High scores.
4

Watchlist the fan-out (Layer 2)

Export “Many Accounts on One Device” and “Many Accounts on One Local IP” from the dashboard and block or step up the Dangerous entities.
5

Tune

Start in logging-only mode, watch where your real logins land, then turn on friction for the highest-risk combinations first.
A guide, not a rule. Layer the conditions: friction should rise as more of them stack.
ConditionSuggested login action
Clean session, normal attempt countAllow
VPN / Proxy / Datacenter in DetailsRequire a CAPTCHA
Tor or Anti-detect Browser in Details, or Score 60+Require a second factor
DeviceID over your failed-attempt limit (across rotated IPs)Rate-limit (HTTP 429)
Device or local IP flagged “Many Accounts on One…”Step up to 2FA or block the device, then review

Next: Login and 2FA

The step-up authentication pattern that pairs with this throttle: when to escalate a risky login to a second factor.