Rate Limiting

Rate limiting is provided as a built-in plugin to protect your API from abuse and ensure fair usage. It registers as pre-auth middleware that runs automatically on every API request.

Rate Limiting

Rate limiting is provided as a built-in plugin to protect your API from abuse and ensure fair usage. It registers as pre-auth middleware that runs automatically on every API request.

Installation

The rate limiting plugin is included with @websideproject/nuxt-auto-api. Enable it via the plugins array:

// nuxt.config.ts
import { createRateLimitPlugin } from '@websideproject/nuxt-auto-api/plugins'

export default defineNuxtConfig({
  autoApi: {
    plugins: [
      createRateLimitPlugin({
        windowMs: 60000,  // 1 minute
        max: 100,         // 100 requests per minute
        byIp: true,
        byUser: false
      })
    ]
  }
})

No additional Nitro plugin or manual middleware setup is needed. The plugin automatically registers pre-auth middleware that checks rate limits before authorization runs.

Migration note: If you were using the legacy createRateLimitExtension with extensions: [...], switch to createRateLimitPlugin with plugins: [...]. The configuration options are the same.

Configuration Options

interface RateLimitConfig {
  // Window size in milliseconds (default: 60000 = 1 minute)
  windowMs?: number

  // Maximum requests per window (default: 100)
  max?: number

  // Rate limit by IP address (default: true)
  byIp?: boolean

  // Rate limit by user ID (default: false)
  byUser?: boolean

  // Custom key generator
  keyGenerator?: (event: any) => string

  // Skip rate limiting for certain requests
  skip?: (event: any) => boolean

  // Custom error message
  message?: string

  // Custom storage backend
  store?: RateLimitStore
}

Examples

Different Limits by Endpoint

// server/plugins/rate-limit.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('request', async (event) => {
    if (event.path.startsWith('/api/auth/')) {
      // Stricter limit for auth endpoints
      await checkRateLimitWithConfig(event, {
        windowMs: 60000,
        max: 5  // Only 5 attempts per minute
      })
    } else if (event.path.startsWith('/api/')) {
      // Normal limit for other endpoints
      await checkRateLimit(event)
    }
  })
})

Rate Limit by User

createRateLimitExtension({
  windowMs: 60000,
  max: 100,
  byIp: false,
  byUser: true  // Rate limit per authenticated user
})

Combined IP and User

createRateLimitExtension({
  windowMs: 60000,
  max: 100,
  byIp: true,
  byUser: true  // Both IP and user must be within limits
})

Custom Key Generator

createRateLimitExtension({
  windowMs: 60000,
  max: 100,
  keyGenerator: (event) => {
    // Rate limit by API key
    const apiKey = event.node.req.headers['x-api-key']
    return `apiKey:${apiKey}`
  }
})

Skip Certain Requests

createRateLimitExtension({
  windowMs: 60000,
  max: 100,
  skip: (event) => {
    // Don't rate limit admins
    return event.context.user?.role === 'admin'
  }
})

Custom Error Message

createRateLimitExtension({
  windowMs: 60000,
  max: 100,
  message: 'You have exceeded the request limit. Please try again in a minute.'
})

Response Headers

Rate limit information is included in response headers:

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1704067200000

When limit is exceeded:

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1704067200000

{
  "statusCode": 429,
  "statusMessage": "Too Many Requests",
  "message": "Too many requests, please try again later",
  "data": {
    "limit": 100,
    "windowMs": 60000,
    "retryAfter": 60
  }
}

Storage Backends

In-Memory (Default)

Suitable for single-server deployments:

createRateLimitExtension({
  // Uses in-memory store by default
})

Pros:

  • Fast
  • No external dependencies
  • Automatic cleanup

Cons:

  • Not shared across multiple servers
  • Lost on server restart
  • Limited to server memory

For multi-server deployments:

// server/utils/redis-rate-limit-store.ts
import { Redis } from 'ioredis'

const redis = new Redis(process.env.REDIS_URL)

export class RedisRateLimitStore {
  async increment(key: string): Promise<number> {
    const count = await redis.incr(key)

    // Set TTL on first increment
    if (count === 1) {
      await redis.expire(key, 60)  // 60 seconds
    }

    return count
  }

  async reset(key: string): Promise<void> {
    await redis.del(key)
  }

  async get(key: string): Promise<number> {
    const count = await redis.get(key)
    return count ? parseInt(count, 10) : 0
  }
}
// nuxt.config.ts
import { createRateLimitExtension } from '@websideproject/nuxt-auto-api/extensions/rate-limiting'
import { RedisRateLimitStore } from './server/utils/redis-rate-limit-store'

export default defineNuxtConfig({
  autoApi: {
    extensions: [
      createRateLimitExtension({
        store: new RedisRateLimitStore()
      })
    ]
  }
})

Pros:

  • Shared across multiple servers
  • Persists across restarts
  • Scalable

Cons:

  • Requires Redis server
  • Slightly slower than in-memory

Advanced Patterns

Different Limits for Different Users

// server/plugins/rate-limit.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('request', async (event) => {
    if (!event.path.startsWith('/api/')) return

    const user = event.context.user

    let limit = 100  // Default

    if (user?.plan === 'premium') {
      limit = 1000
    } else if (user?.plan === 'enterprise') {
      limit = 10000
    }

    await checkRateLimitWithConfig(event, {
      windowMs: 60000,
      max: limit,
      keyGenerator: () => `user:${user?.id || 'anonymous'}`
    })
  })
})

Burst vs Sustained Rate

// Allow bursts but limit sustained rate
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('request', async (event) => {
    if (!event.path.startsWith('/api/')) return

    // Short-term burst limit: 20 requests per 10 seconds
    await checkRateLimitWithConfig(event, {
      windowMs: 10000,
      max: 20,
      keyGenerator: (e) => `burst:${getClientIP(e)}`
    })

    // Long-term sustained limit: 100 requests per minute
    await checkRateLimitWithConfig(event, {
      windowMs: 60000,
      max: 100,
      keyGenerator: (e) => `sustained:${getClientIP(e)}`
    })
  })
})

Progressive Rate Limiting

// Increase limits based on usage
const getUserRateLimit = async (userId: string) => {
  const usage = await db.query.usage.findFirst({
    where: eq(usage.userId, userId)
  })

  if (!usage) return 100

  // Increase limit for active users
  if (usage.requestCount > 10000) {
    return 500
  } else if (usage.requestCount > 1000) {
    return 200
  }

  return 100
}

Per-Endpoint Limits

const ENDPOINT_LIMITS = {
  '/api/search': { windowMs: 60000, max: 10 },
  '/api/export': { windowMs: 3600000, max: 5 },
  '/api/upload': { windowMs: 60000, max: 20 },
  default: { windowMs: 60000, max: 100 }
}

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('request', async (event) => {
    const config = ENDPOINT_LIMITS[event.path] || ENDPOINT_LIMITS.default

    await checkRateLimitWithConfig(event, config)
  })
})

Monitoring

Track Rate Limit Hits

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('request', async (event) => {
    try {
      await checkRateLimit(event)
    } catch (error) {
      if (error.statusCode === 429) {
        // Log rate limit hit
        console.warn('Rate limit exceeded:', {
          ip: getClientIP(event),
          path: event.path,
          userId: event.context.user?.id
        })

        // Track in analytics
        await trackEvent('rate_limit_exceeded', {
          ip: getClientIP(event),
          path: event.path
        })
      }
      throw error
    }
  })
})

Prometheus Metrics

import { Counter } from 'prom-client'

const rateLimitCounter = new Counter({
  name: 'rate_limit_exceeded_total',
  help: 'Total number of rate limit exceeded errors',
  labelNames: ['path', 'user_type']
})

// In your rate limit check
if (error.statusCode === 429) {
  rateLimitCounter.inc({
    path: event.path,
    user_type: event.context.user ? 'authenticated' : 'anonymous'
  })
}

Testing

Disable in Development

createRateLimitExtension({
  skip: (event) => {
    return process.env.NODE_ENV === 'development'
  }
})

Test Rate Limiting

// test/rate-limiting.test.ts
import { describe, it, expect } from 'vitest'

describe('Rate Limiting', () => {
  it('should block requests after limit', async () => {
    const requests = []

    // Make 101 requests (limit is 100)
    for (let i = 0; i < 101; i++) {
      requests.push(
        $fetch('/api/users').catch(e => e)
      )
    }

    const results = await Promise.all(requests)

    // Last request should be rate limited
    expect(results[100].statusCode).toBe(429)
  })
})

Client-Side Handling

Respect Rate Limits

// composables/useAutoApiWithRateLimit.ts
export const useAutoApiWithRateLimit = (resource: string) => {
  const { fetch, ...rest } = useAutoApiFetch(resource)

  const fetchWithRetry = async (options: any) => {
    try {
      return await fetch(options)
    } catch (error) {
      if (error.statusCode === 429) {
        const retryAfter = error.data?.retryAfter || 60

        // Show user-friendly message
        console.warn(`Rate limited. Retrying in ${retryAfter} seconds...`)

        // Wait and retry
        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000))
        return await fetch(options)
      }
      throw error
    }
  }

  return { ...rest, fetch: fetchWithRetry }
}

Display Rate Limit Info

<script setup>
const rateLimitInfo = ref(null)

onMounted(() => {
  // Check headers after request
  const { data, response } = await useAutoApiFetch('users')

  if (response.headers) {
    rateLimitInfo.value = {
      limit: response.headers.get('X-RateLimit-Limit'),
      remaining: response.headers.get('X-RateLimit-Remaining'),
      reset: new Date(parseInt(response.headers.get('X-RateLimit-Reset')))
    }
  }
})
</script>

<template>
  <div v-if="rateLimitInfo">
    {{ rateLimitInfo.remaining }} / {{ rateLimitInfo.limit }} requests remaining
    (resets at {{ rateLimitInfo.reset.toLocaleTimeString() }})
  </div>
</template>

Best Practices

  1. Start conservative: Begin with lower limits and increase based on usage
  2. Monitor closely: Track 429 errors to find the right limits
  3. Use Redis in production: In-memory store doesn't scale
  4. Differentiate users: Premium users get higher limits
  5. Protect expensive endpoints: Lower limits for resource-intensive operations
  6. Clear error messages: Help users understand what happened
  7. Provide headers: Include X-RateLimit-* headers
  8. Test thoroughly: Ensure rate limiting doesn't break normal usage
  9. Document limits: Tell users what the limits are
  10. Consider quotas: Combine with daily/monthly quotas for paid tiers

Troubleshooting

Rate Limit Not Working

Check that:

  1. Extension is registered in nuxt.config.ts
  2. Nitro plugin is applying checkRateLimit
  3. Redis is connected (if using Redis store)
  4. Skip function isn't excluding all requests

Too Many False Positives

Adjust limits:

createRateLimitExtension({
  windowMs: 60000,
  max: 200  // Increase limit
})

Or use per-user limits instead of per-IP:

createRateLimitExtension({
  byIp: false,
  byUser: true
})

Memory Issues

Switch to Redis:

  • In-memory store grows with traffic
  • Redis provides bounded memory usage

Alternatives

  • Cloudflare: Rate limiting at the edge
  • nginx: Rate limiting at reverse proxy
  • API Gateway: AWS API Gateway, Kong, etc.
  • Custom middleware: Full control but more work

Migration Path

From custom rate limiting:

// Before: Custom implementation
const requests = new Map()

export default defineEventHandler(async (event) => {
  const ip = getClientIP(event)
  const count = requests.get(ip) || 0

  if (count > 100) {
    throw createError({ statusCode: 429, message: 'Too many requests' })
  }

  requests.set(ip, count + 1)
  // ... rest of handler
})

// After: Use extension
createRateLimitExtension({
  max: 100,
  byIp: true
})

Benefits:

  • Automatic window management
  • Response headers
  • Multiple storage backends
  • Better error messages
  • Configuration options

Need a Landing Page?

Modern landing pages with optional modules (blog, docs, forms, i18n). Let's discuss your project.

Build Your MVP

Full-stack SaaS development. Expert in database design, multi-tenancy, and scalable architecture.

Deployment Help

Dockerize your backend, set up CI/CD pipelines, deploy to Cloudflare or Hetzner. Early-stage setup.

Suggest a SaaS Tool

Missing a calculator or tool? Suggest what you'd like to see on our site.