Unlike a brute force attack, the attacker is not guessing. They already hold real passwords from a breach somewhere else and are testing which of those people also have an account with you. Your app does not need to have been breached for your users to be taken over this way.
It is hard to spot because each account usually sees only one or two attempts, often from many different IP addresses, so a simple per-account failure counter does not trigger. The signs are a rise in failed logins overall, logins from unusual places, and attempts against email addresses that have never signed up.
Two-factor authentication is the strongest defense, because a reused password alone is no longer enough. Beyond that, rate limit login by IP address and by device, check new passwords against lists of known breached passwords, add a challenge such as a CAPTCHA when traffic looks automated, and tell users when a login happens from a new device.