[{"data":1,"prerenderedAt":349},["ShallowReactive",2],{"blog-how-to/rate-limiting-auth":3},{"id":4,"title":5,"body":6,"category":329,"date":330,"dateModified":331,"description":332,"draft":333,"extension":334,"faq":335,"featured":333,"headerVariant":336,"image":335,"keywords":335,"meta":337,"navigation":338,"ogDescription":339,"ogTitle":335,"path":340,"readTime":335,"schemaOrg":341,"schemaType":342,"seo":343,"sitemap":344,"stem":345,"tags":346,"twitterCard":347,"__hash__":348},"blog/blog/how-to/rate-limiting-auth.md","How to Implement Rate Limiting for Authentication",{"type":7,"value":8,"toc":313},"minimark",[9,13,17,21,27,30,43,48,51,55,75,88,101,114,127,140,153,182,186,220,224,229,232,236,239,243,246,250,253,275,294],[10,11],"category-badge",{"category":12},"How-To Guide",[14,15,5],"h1",{"id":16},"how-to-implement-rate-limiting-for-authentication",[18,19,20],"p",{},"Stop brute force attacks before they start",[22,23,24],"tldr",{},[18,25,26],{},"TL;DR (20 minutes):\nRate limit by both IP (10/15min) and email/username (5/15min). Use Redis with Upstash or similar. Lock accounts after 5 failed attempts for 15 minutes. Add progressive delays (1s, 2s, 4s...) between attempts. Always return generic errors to prevent enumeration.",[18,28,29],{},"Prerequisites:",[31,32,33,37,40],"ul",{},[34,35,36],"li",{},"Authentication system to protect",[34,38,39],{},"Redis or similar for state storage",[34,41,42],{},"Understanding of your traffic patterns",[44,45,47],"h2",{"id":46},"why-this-matters","Why This Matters",[18,49,50],{},"Without rate limiting, attackers can try thousands of password combinations per second. Credential stuffing attacks use leaked passwords from other sites - a single IP can attempt millions of logins targeting different accounts. Rate limiting is essential protection.",[44,52,54],{"id":53},"step-by-step-guide","Step-by-Step Guide",[56,57,59,64],"step",{"number":58},"1",[60,61,63],"h3",{"id":62},"set-up-upstash-redis","Set up Upstash Redis",[65,66,71],"pre",{"className":67,"code":69,"language":70},[68],"language-text","npm install @upstash/ratelimit @upstash/redis\n\n// lib/redis.ts\nimport { Redis } from '@upstash/redis';\n\nexport const redis = new Redis({\n  url: process.env.UPSTASH_REDIS_REST_URL,\n  token: process.env.UPSTASH_REDIS_REST_TOKEN\n});\n","text",[72,73,69],"code",{"__ignoreMap":74},"",[56,76,78,82],{"number":77},"2",[60,79,81],{"id":80},"create-rate-limiters","Create rate limiters",[65,83,86],{"className":84,"code":85,"language":70},[68],"import { Ratelimit } from '@upstash/ratelimit';\nimport { redis } from './redis';\n\n// Global IP rate limit (loose - catches distributed attacks)\nexport const ipRateLimiter = new Ratelimit({\n  redis,\n  limiter: Ratelimit.slidingWindow(20, '15 m'),\n  prefix: 'ratelimit:ip'\n});\n\n// Per-email rate limit (strict - protects individual accounts)\nexport const emailRateLimiter = new Ratelimit({\n  redis,\n  limiter: Ratelimit.slidingWindow(5, '15 m'),\n  prefix: 'ratelimit:email'\n});\n\n// Failed attempt tracker (for progressive delays)\nexport const failedAttemptLimiter = new Ratelimit({\n  redis,\n  limiter: Ratelimit.slidingWindow(10, '1 h'),\n  prefix: 'ratelimit:failed'\n});\n",[72,87,85],{"__ignoreMap":74},[56,89,91,95],{"number":90},"3",[60,92,94],{"id":93},"implement-the-rate-limiting-middleware","Implement the rate limiting middleware",[65,96,99],{"className":97,"code":98,"language":70},[68],"interface RateLimitResult {\n  allowed: boolean;\n  remaining: number;\n  resetAt: Date;\n  retryAfter?: number;\n}\n\nasync function checkAuthRateLimits(\n  email: string,\n  ip: string\n): Promise {\n  // Check IP limit\n  const ipResult = await ipRateLimiter.limit(ip);\n  if (!ipResult.success) {\n    return {\n      allowed: false,\n      remaining: 0,\n      resetAt: new Date(ipResult.reset),\n      retryAfter: Math.ceil((ipResult.reset - Date.now()) / 1000)\n    };\n  }\n\n  // Check email-specific limit\n  const emailResult = await emailRateLimiter.limit(email.toLowerCase());\n  if (!emailResult.success) {\n    return {\n      allowed: false,\n      remaining: 0,\n      resetAt: new Date(emailResult.reset),\n      retryAfter: Math.ceil((emailResult.reset - Date.now()) / 1000)\n    };\n  }\n\n  return {\n    allowed: true,\n    remaining: Math.min(ipResult.remaining, emailResult.remaining),\n    resetAt: new Date(Math.max(ipResult.reset, emailResult.reset))\n  };\n}\n",[72,100,98],{"__ignoreMap":74},[56,102,104,108],{"number":103},"4",[60,105,107],{"id":106},"add-progressive-delays","Add progressive delays",[65,109,112],{"className":110,"code":111,"language":70},[68],"async function getProgressiveDelay(email: string): Promise {\n  const key = `auth:delay:${email.toLowerCase()}`;\n\n  // Get current attempt count\n  const attempts = await redis.get(key) || 0;\n\n  // Exponential backoff: 0, 1, 2, 4, 8, 16... seconds (max 30)\n  const delay = Math.min(Math.pow(2, attempts - 1), 30) * 1000;\n\n  return attempts > 0 ? delay : 0;\n}\n\nasync function recordFailedAttempt(email: string): Promise {\n  const key = `auth:delay:${email.toLowerCase()}`;\n\n  await redis.pipeline()\n    .incr(key)\n    .expire(key, 15 * 60)  // Reset after 15 minutes\n    .exec();\n}\n\nasync function clearFailedAttempts(email: string): Promise {\n  const key = `auth:delay:${email.toLowerCase()}`;\n  await redis.del(key);\n}\n\n// Apply delay before processing\nasync function applyProgressiveDelay(email: string): Promise {\n  const delay = await getProgressiveDelay(email);\n  if (delay > 0) {\n    await new Promise(resolve => setTimeout(resolve, delay));\n  }\n}\n",[72,113,111],{"__ignoreMap":74},[56,115,117,121],{"number":116},"5",[60,118,120],{"id":119},"implement-account-lockout","Implement account lockout",[65,122,125],{"className":123,"code":124,"language":70},[68],"const LOCKOUT_THRESHOLD = 5;\nconst LOCKOUT_DURATION = 15 * 60 * 1000; // 15 minutes\n\nasync function checkAccountLockout(email: string): Promise\u003C{\n  locked: boolean;\n  unlocksAt?: Date;\n}> {\n  const lockKey = `auth:locked:${email.toLowerCase()}`;\n  const lockedUntil = await redis.get(lockKey);\n\n  if (lockedUntil && lockedUntil > Date.now()) {\n    return {\n      locked: true,\n      unlocksAt: new Date(lockedUntil)\n    };\n  }\n\n  return { locked: false };\n}\n\nasync function lockAccountIfNeeded(email: string): Promise {\n  const attemptKey = `auth:attempts:${email.toLowerCase()}`;\n  const lockKey = `auth:locked:${email.toLowerCase()}`;\n\n  // Increment failed attempts\n  const attempts = await redis.incr(attemptKey);\n  await redis.expire(attemptKey, 15 * 60);\n\n  if (attempts >= LOCKOUT_THRESHOLD) {\n    // Lock the account\n    const unlocksAt = Date.now() + LOCKOUT_DURATION;\n    await redis.set(lockKey, unlocksAt, { ex: Math.ceil(LOCKOUT_DURATION / 1000) });\n\n    // Clear attempt counter\n    await redis.del(attemptKey);\n\n    return true;  // Account is now locked\n  }\n\n  return false;\n}\n\nasync function unlockAccount(email: string): Promise {\n  const lockKey = `auth:locked:${email.toLowerCase()}`;\n  const attemptKey = `auth:attempts:${email.toLowerCase()}`;\n\n  await redis.del(lockKey);\n  await redis.del(attemptKey);\n}\n",[72,126,124],{"__ignoreMap":74},[56,128,130,134],{"number":129},"6",[60,131,133],{"id":132},"complete-login-endpoint-with-rate-limiting","Complete login endpoint with rate limiting",[65,135,138],{"className":136,"code":137,"language":70},[68],"async function login(req, res) {\n  const { email, password } = req.body;\n  const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';\n\n  // Step 1: Check rate limits\n  const rateLimit = await checkAuthRateLimits(email, ip);\n  if (!rateLimit.allowed) {\n    return res.status(429).json({\n      error: 'Too many login attempts. Please try again later.',\n      retryAfter: rateLimit.retryAfter\n    });\n  }\n\n  // Step 2: Check account lockout\n  const lockout = await checkAccountLockout(email);\n  if (lockout.locked) {\n    return res.status(423).json({\n      error: 'Account temporarily locked due to too many failed attempts.',\n      unlocksAt: lockout.unlocksAt\n    });\n  }\n\n  // Step 3: Apply progressive delay\n  await applyProgressiveDelay(email);\n\n  // Step 4: Verify credentials\n  const user = await prisma.user.findUnique({\n    where: { email: email.toLowerCase() }\n  });\n\n  // Generic error prevents enumeration\n  if (!user || !await bcrypt.compare(password, user.passwordHash)) {\n    // Record failure\n    await recordFailedAttempt(email);\n    const locked = await lockAccountIfNeeded(email);\n\n    if (locked) {\n      return res.status(423).json({\n        error: 'Account temporarily locked due to too many failed attempts.',\n        unlocksAt: new Date(Date.now() + LOCKOUT_DURATION)\n      });\n    }\n\n    return res.status(401).json({\n      error: 'Invalid email or password.'\n    });\n  }\n\n  // Step 5: Success - clear failures and create session\n  await clearFailedAttempts(email);\n\n  const session = await createSession(user.id, req);\n\n  return res.json({\n    success: true,\n    user: { id: user.id, email: user.email }\n  });\n}\n",[72,139,137],{"__ignoreMap":74},[56,141,143,147],{"number":142},"7",[60,144,146],{"id":145},"add-rate-limit-headers","Add rate limit headers",[65,148,151],{"className":149,"code":150,"language":70},[68],"// Middleware to add rate limit headers\nasync function addRateLimitHeaders(req, res, next) {\n  const ip = req.ip;\n  const result = await ipRateLimiter.limit(ip);\n\n  // Standard rate limit headers\n  res.setHeader('X-RateLimit-Limit', result.limit);\n  res.setHeader('X-RateLimit-Remaining', result.remaining);\n  res.setHeader('X-RateLimit-Reset', result.reset);\n\n  if (!result.success) {\n    res.setHeader('Retry-After', Math.ceil((result.reset - Date.now()) / 1000));\n    return res.status(429).json({\n      error: 'Rate limit exceeded'\n    });\n  }\n\n  next();\n}\n",[72,152,150],{"__ignoreMap":74},[154,155,156,159],"warning-box",{},[18,157,158],{},"Rate Limiting Best Practices:",[31,160,161,164,167,170,173,176,179],{},[34,162,163],{},"Rate limit by both IP and username/email - attackers can rotate IPs",[34,165,166],{},"Use sliding window algorithm - more accurate than fixed windows",[34,168,169],{},"Return 429 status code with Retry-After header",[34,171,172],{},"Never reveal whether an email exists in error messages",[34,174,175],{},"Consider CAPTCHA after a few failed attempts",[34,177,178],{},"Monitor and alert on unusual rate limit triggers",[34,180,181],{},"Whitelist your own services/IPs if needed",[44,183,185],{"id":184},"how-to-verify-it-worked","How to Verify It Worked",[187,188,189,196,202,208,214],"ol",{},[34,190,191,195],{},[192,193,194],"strong",{},"Test IP limiting:"," Make 25+ requests from same IP - should be blocked",[34,197,198,201],{},[192,199,200],{},"Test email limiting:"," Try 6+ logins for same email - should be blocked",[34,203,204,207],{},[192,205,206],{},"Test lockout:"," Fail 5 logins - account should lock",[34,209,210,213],{},[192,211,212],{},"Test progressive delay:"," Each failure should take longer",[34,215,216,219],{},[192,217,218],{},"Test legitimate login:"," After delay expires, verify normal login works",[44,221,223],{"id":222},"common-errors-troubleshooting","Common Errors & Troubleshooting",[225,226,228],"h4",{"id":227},"rate-limiting-not-working-consistently","Rate limiting not working consistently",[18,230,231],{},"Check Redis connection. In serverless, ensure you're using the same Redis instance across all function instances.",[225,233,235],{"id":234},"legitimate-users-getting-blocked","Legitimate users getting blocked",[18,237,238],{},"Your limits may be too strict. Monitor actual usage patterns and adjust. Consider higher limits for verified accounts.",[225,240,242],{"id":241},"attackers-bypassing-ip-limits","Attackers bypassing IP limits",[18,244,245],{},"They're using rotating proxies. Implement per-email limits, CAPTCHA after failures, and consider device fingerprinting.",[225,247,249],{"id":248},"redis-connection-errors","Redis connection errors",[18,251,252],{},"Add fallback behavior - if Redis is down, either fail open (allow login) or fail closed (deny all). Choose based on your security needs.",[254,255,256,263,269],"faq-section",{},[257,258,260],"faq-item",{"question":259},"What limits should I set?",[18,261,262],{},"Start with: 20 requests/15min per IP, 5 requests/15min per email. Lock account after 5 failures for 15 minutes. Adjust based on your user behavior and security needs.",[257,264,266],{"question":265},"Should I use CAPTCHA instead?",[18,267,268],{},"Use both. Rate limiting is your first line of defense. Show CAPTCHA after 2-3 failed attempts. This balances security with UX.",[257,270,272],{"question":271},"How do I handle shared IP addresses (offices, universities)?",[18,273,274],{},"Keep per-IP limits reasonable (not too strict), rely more on per-email limits, and consider adding CAPTCHA. You can also whitelist known corporate IPs.",[18,276,277,280,285,286,285,290],{},[192,278,279],{},"Related guides:",[281,282,284],"a",{"href":283},"/blog/how-to/secure-login-form","Secure Login Form"," ·\n",[281,287,289],{"href":288},"/blog/how-to/implement-rate-limiting","General Rate Limiting",[281,291,293],{"href":292},"/blog/how-to/password-reset-security","Password Reset Security",[295,296,297,303,308],"related-articles",{},[298,299],"related-card",{"description":300,"href":301,"title":302},"Step-by-step guide to PostgreSQL role-based access control. Create users, assign permissions, and implement least-privil","/blog/how-to/postgresql-roles","How to Set Up PostgreSQL Roles and Permissions",[298,304],{"description":305,"href":306,"title":307},"Step-by-step guide to preventing SQL injection. Parameterized queries, ORMs, input validation, and common mistakes that ","/blog/how-to/prevent-sql-injection","How to Prevent SQL Injection in Your App",[298,309],{"description":310,"href":311,"title":312},"Step-by-step guide to securing your Prisma ORM setup. Prevent injection attacks, handle raw queries safely, and implemen","/blog/how-to/prisma-security","How to Secure Prisma ORM",{"title":74,"searchDepth":314,"depth":314,"links":315},2,[316,317,327,328],{"id":46,"depth":314,"text":47},{"id":53,"depth":314,"text":54,"children":318},[319,321,322,323,324,325,326],{"id":62,"depth":320,"text":63},3,{"id":80,"depth":320,"text":81},{"id":93,"depth":320,"text":94},{"id":106,"depth":320,"text":107},{"id":119,"depth":320,"text":120},{"id":132,"depth":320,"text":133},{"id":145,"depth":320,"text":146},{"id":184,"depth":314,"text":185},{"id":222,"depth":314,"text":223},"how-to","2026-01-26","2026-02-17","Step-by-step guide to rate limiting authentication endpoints. Prevent brute force attacks, credential stuffing, and account enumeration.",false,"md",null,"yellow",{},true,"Protect login endpoints from brute force and credential stuffing attacks.","/blog/how-to/rate-limiting-auth","[object Object]","HowTo",{"title":5,"description":332},{"loc":340},"blog/how-to/rate-limiting-auth",[],"summary_large_image","KX6kLnPjESgZXoBtrBqMfCIHG9JRRneowaOawOD-Zvw",1775843927193]