On this page

Authentication Rate Limiting (Bun)

Using Node.js? See the Node.js Auth Example for @joint-ops/hitlimit.

Protecting authentication endpoints is critical. This example shows how to implement layered rate limiting on Bun that prevents credential stuffing, brute force attacks, and account takeover attempts.

Scenario

  • Login: 5 attempts per 15 minutes (per email + IP)
  • Registration: 3 accounts per hour (per IP)
  • Password reset: 3 requests per hour (prevents enumeration)

Implementation

src/rate-limits/auth.ts
import { hitlimit } from '@joint-ops/hitlimit-bun'
import { redisStore } from '@joint-ops/hitlimit-bun/stores/redis'

const redis = redisStore({
  url: Bun.env.REDIS_URL,
  keyPrefix: 'auth:rl:'
})

// Login - strict limits per email + IP
export const loginLimit = hitlimit({
  store: redis,
  limit: 5,
  window: '15m',
  key: async (req) => {
    const body = await req.clone().json()
    const email = body.email?.toLowerCase() ?? ''
    const ip = req.headers.get('x-forwarded-for') ?? 'unknown'
    return `login:${email}:${ip}`
  },
  response: (info) => ({
    error: 'LOGIN_RATE_LIMITED',
    message: `Too many login attempts. Try again in ${Math.ceil(info.resetIn / 60)} minutes.`,
    resetIn: info.resetIn
  }),
  onStoreError: () => 'deny' // Security-critical: always fail closed
})

// Registration - prevent spam accounts
export const registrationLimit = hitlimit({
  store: redis,
  limit: 3,
  window: '1h',
  key: (req) => {
    const ip = req.headers.get('x-forwarded-for') ?? 'unknown'
    return `register:${ip}`
  },
  response: () => ({
    error: 'REGISTRATION_LIMIT',
    message: 'Too many registration attempts. Please try again later.'
  }),
  onStoreError: () => 'deny'
})

// Password reset - strict (prevent enumeration + spam)
export const passwordResetLimit = hitlimit({
  store: redis,
  limit: 3,
  window: '1h',
  key: async (req) => {
    const body = await req.clone().json()
    const email = body.email?.toLowerCase() ?? ''
    const ip = req.headers.get('x-forwarded-for') ?? 'unknown'
    return `password-reset:${email}:${ip}`
  },
  response: () => ({
    // Intentionally vague to prevent email enumeration
    message: 'If an account exists with that email, a reset link has been sent.'
  }),
  onStoreError: () => 'deny'
})

Route Handling

Apply the rate limiters inside a Bun.serve fetch handler, routing by URL pathname:

src/server.ts
import { loginLimit, registrationLimit, passwordResetLimit } from './rate-limits/auth'

Bun.serve({
  port: 3000,
  async fetch(req) {
    const url = new URL(req.url)

    // POST /auth/login
    if (url.pathname === '/auth/login' && req.method === 'POST') {
      const limited = await loginLimit(req)
      if (limited) return limited
      // ... handle login
      return Response.json({ ok: true })
    }

    // POST /auth/register
    if (url.pathname === '/auth/register' && req.method === 'POST') {
      const limited = await registrationLimit(req)
      if (limited) return limited
      // ... handle registration
      return Response.json({ ok: true })
    }

    // POST /auth/password/forgot
    if (url.pathname === '/auth/password/forgot' && req.method === 'POST') {
      const limited = await passwordResetLimit(req)
      if (limited) return limited
      // ... handle password reset
      return Response.json({ ok: true })
    }

    return new Response('Not Found', { status: 404 })
  }
})

Key Points

  • Always fail closed for auth: Every limiter uses onStoreError: () => 'deny'. If your Redis store is unavailable, it is safer to reject requests than to allow unlimited login attempts.
  • Clone the request before reading the body: req.clone().json() ensures the original request body remains available for your route handler.
  • Combine email + IP keys for login: This prevents both credential stuffing (many emails from one IP) and targeted brute force (many IPs targeting one email).
  • Use vague password reset messages: A generic "If an account exists..." response prevents attackers from using the endpoint to enumerate valid email addresses.
  • Use Bun.env for configuration: Access environment variables natively through Bun.env.REDIS_URL instead of process.env.
  • Separate key prefixes per endpoint: Each limiter targets a specific action (login, register, password-reset), so one endpoint being rate limited does not affect the others.