What a real credential stuffing attack looks like on a small app
Credential stuffing is not a sophisticated attack. It does not involve guessing passwords, exploiting application vulnerabilities, or any technical skill beyond running a script. An attacker acquires a list of email addresses and passwords that were leaked in a breach of a different service — LinkedIn, Adobe, a forum, a shopping site. These lists are sold openly and cheaply. The attacker then runs automated software that submits each pair to your login form and records which ones succeed. The entire premise of the attack is that users reuse passwords across multiple services, which they do at a documented rate of over 60%. A list of one million email-password pairs from a 2019 breach will still produce thousands of valid logins on apps that have not implemented defenses, because the users have not changed their passwords in six years.
In your logs, a credential stuffing attack looks like a high volume of failed authentication requests arriving from a range of IP addresses in rapid succession. The key distinguishing characteristics are: the volume (hundreds to thousands of attempts per hour), the distribution across IPs (attackers rotate through residential proxies to avoid simple IP-based blocking), the targeting of email providers (the lists are often sorted by provider, so you may see a wave of Gmail addresses followed by a wave of Outlook addresses), and the timing pattern (attempts often come in bursts that match the throughput of the attacker's tool). Without log monitoring set up before the attack, you may not notice it until your server load spikes or a user reports that their account was accessed from an unrecognized location.
Small apps are actively targeted by credential stuffing attacks, not ignored in favor of larger targets. The tooling that attackers use does not discriminate by app size — it simply runs a list of credentials against every URL in a target list, and small apps get on those lists because they are discoverable and assumed to lack defenses. The assumption is often correct. A small SaaS app that launched six months ago with an AI coding tool is unlikely to have rate limiting on its login endpoint, unlikely to have anomaly detection in place, and is more likely to have users who signed up with the same password they use everywhere. The attacker does not need to care about your specific app — they care about whether their credential list has any valid hits, and they will find out by testing.
The damage from a successful credential stuffing hit scales with what the compromised account has access to. For a SaaS app that stores payment methods, a successful login gives the attacker access to whatever billing actions the account can take — potentially fraudulent charges or refund abuse. For an app with user-generated content, it means the attacker can read private messages, download private files, or impersonate the compromised user to their contacts. For an app with team or organizational features, a compromised account may give access to an entire organization's data. The compromised user rarely notices immediately — the attacker is careful not to trigger obvious alerts like password changes or email address updates, preferring to quietly harvest value from the account.
Defending against credential stuffing requires rate limiting that operates at the IP level, not the account level. Account-level lockouts are bypassed by simply moving to a different email from the list. IP-level rate limiting limits how many authentication attempts a single source can make per time window, which slows automated attacks to a rate where they are no longer economically viable. In Next.js, this is typically implemented using a Redis store in middleware or an API route. Supplement rate limiting with a threshold-based alert: if you see more than a configurable number of failed logins in a five-minute window, send yourself a notification. Add HaveIBeenPwned API integration to check submitted passwords against known-breached credential databases and prompt users with hits to change their password.
If you are being actively attacked right now and your logs are filling with failed authentication attempts, the immediate actions are: block the IP ranges or ASNs generating the most volume at the network or CDN level while you implement application-level rate limiting; add a temporary CAPTCHA to the login form to break automation; and enable Supabase Auth's built-in login attempt monitoring if you use it. None of these are permanent solutions, but they will slow the attack while you put proper rate limiting in place. Once rate limiting is deployed, the attack will naturally stop — it becomes computationally expensive for the attacker relative to the returns, and they move on to softer targets.