Real World Example

Where You See This

  • Banking apps (SBI, HDFC): card or netbanking blocked for 24 hours after 3 wrong PINs/passwords.
  • GitHub, Twitter/X APIs: X-RateLimit-Remaining and 429 with Retry-After headers.
  • Flipkart / Amazon OTP: "You have exceeded the maximum OTP attempts. Try after 30 minutes."

ShopEase Login Defence in Layers

LayerLimitOn exceed
Cloudflare / gateway100 requests / minute per IP to /api/auth/**429
LoginRateLimitFilter (Redis)10 login attempts / minute per IP429 + Retry-After
Per account3 failuresCAPTCHA required
Per account5 failureslocked 15 min, email "Was this you?"
OTP send3 SMS / 10 min per phone, 10 / day429, message to wait
Breached password checkon login and signupforce password reset
@Bean
ProxyManager<String> redisProxyManager(RedisClient redisClient) {
    StatefulRedisConnection<String, byte[]> connection =
            redisClient.connect(RedisCodec.of(StringCodec.UTF8, ByteArrayCodec.INSTANCE));
    return LettuceBasedProxyManager.builderFor(connection)
            .withExpirationStrategy(ExpirationAfterWriteStrategy
                    .basedOnTimeForRefillingBucketUpToMax(Duration.ofMinutes(5)))
            .build();
}

// In the filter: one shared bucket per IP across every ShopEase instance
Bucket bucket = proxyManager.builder().build("login:" + clientIp, () -> BucketConfiguration.builder()
        .addLimit(limit -> limit.capacity(10).refillGreedy(10, Duration.ofMinutes(1)))
        .build());

Log output

Security log when a credential-stuffing bot hits ShopEase:

grep -E "RATE_LIMITED|ACCOUNT_LOCKED" logs/security.log | tail -3

Example output

2026-03-01T02:14:09.310+05:30  WARN [shopease] RATE_LIMITED ip=185.220.101.47 path=/api/auth/login retryAfter=6s
2026-03-01T02:14:09.512+05:30  WARN [shopease] RATE_LIMITED ip=185.220.101.47 path=/api/auth/login retryAfter=6s
2026-03-01T02:16:44.902+05:30  WARN [shopease] ACCOUNT_LOCKED user=r***@gmail.com failures=5 lockedUntil=2026-03-01T02:31:44+05:30