Rate Limiting API Protection - nself-org/nchat GitHub Wiki
Rate Limiting & API Protection
Complete documentation for the nself-chat rate limiting and API protection system.
Overview
The application implements a comprehensive multi-layered security approach:
- IP Blocking - Whitelist/blacklist management with automatic abuse detection
- Rate Limiting - Sliding window algorithm with Redis support
- CSRF Protection - Double-submit cookie pattern
- Authentication - JWT-based auth with role-based access control
Architecture
Request Flow:
┌─────────────────────────────────────────────────────────────────┐
│ Client Request │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Layer 1: Next.js Middleware (Edge) │
│ • IP Blocking Check (whitelist/blacklist) │
│ • Penalty Box Check (temporary blocks) │
│ • Rate Limiting (edge-compatible) │
│ • Security Headers (CSP, HSTS) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Layer 2: API Route Middleware (Server) │
│ • Error Handling (withErrorHandler) │
│ • Logging (withLogging) │
│ • CSRF Protection (withCsrfProtection) │
│ • Authentication (withAuth) │
│ • Rate Limiting (withRateLimit) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Business Logic Handler │
└─────────────────────────────────────────────────────────────────┘
Rate Limiting
Algorithms
1. Sliding Window (Default)
The sliding window algorithm provides smooth rate limiting by tracking individual request timestamps:
import { rateLimiter, RATE_LIMIT_PRESETS } from '@/lib/api/rate-limiter'
// Check rate limit
const result = await rateLimiter.check('user:123', RATE_LIMIT_PRESETS.MESSAGE_SEND)
if (!result.allowed) {
// Rate limited - return 429
return Response.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'Retry-After': result.retryAfter.toString(),
},
}
)
}
How it works:
- Stores timestamp for each request in a sorted set (Redis) or array (in-memory)
- Removes requests outside the time window
- Counts remaining requests in the current window
- Smooth rate limiting without sudden resets
2. Token Bucket
The token bucket algorithm allows controlled bursts:
const result = await rateLimiter.checkTokenBucket('user:123', {
maxRequests: 10,
windowSeconds: 60,
burst: 5, // Allow bursts up to 15 total
})
How it works:
- Tokens refill at a constant rate
- Each request consumes 1 token
- Allows bursts when tokens are available
- Better UX for bursty traffic patterns
Rate Limit Presets
Pre-configured rate limits for common scenarios:
// Authentication endpoints
RATE_LIMIT_PRESETS.AUTH // 5/min
RATE_LIMIT_PRESETS.AUTH_SIGNUP // 3/hour
RATE_LIMIT_PRESETS.AUTH_RESET // 3/15min
// Message operations
RATE_LIMIT_PRESETS.MESSAGE_SEND // 10/min + 5 burst
RATE_LIMIT_PRESETS.MESSAGE_EDIT // 20/min
// File uploads
RATE_LIMIT_PRESETS.FILE_UPLOAD // 5/min
RATE_LIMIT_PRESETS.FILE_UPLOAD_LARGE // 2/5min
// Search
RATE_LIMIT_PRESETS.SEARCH // 20/min + 10 burst
// AI operations
RATE_LIMIT_PRESETS.AI_QUERY // 10/min
// General API
RATE_LIMIT_PRESETS.API_USER // 100/min + 20 burst
RATE_LIMIT_PRESETS.API_IP // 500/min
// Other
RATE_LIMIT_PRESETS.GRAPHQL // 100/min
RATE_LIMIT_PRESETS.WEBHOOK // 50/min
RATE_LIMIT_PRESETS.EMAIL_SEND // 10/hour
RATE_LIMIT_PRESETS.EXPORT // 3/hour
Usage in API Routes
Basic Rate Limiting
import { applyRateLimit, RATE_LIMIT_PRESETS } from '@/lib/api/rate-limiter'
export async function POST(request: NextRequest) {
// Apply rate limit
const result = await applyRateLimit(
request,
RATE_LIMIT_PRESETS.MESSAGE_SEND,
'user:123' // Optional custom identifier
)
if (!result.allowed) {
return Response.json(
{
error: 'Too many requests',
retryAfter: result.retryAfter,
},
{
status: 429,
headers: {
'X-RateLimit-Limit': result.limit.toString(),
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': result.reset.toString(),
'Retry-After': result.retryAfter?.toString() || '60',
},
}
)
}
// Continue with business logic...
}
Using Middleware
import { compose, withAuth, withRateLimit } from '@/lib/api/middleware'
export const POST = compose(
withErrorHandler,
withRateLimit({ limit: 10, window: 60 }),
withAuth
)(async (request, context) => {
// Your handler code - rate limit already applied
return Response.json({ success: true })
})
Custom Rate Limits
const customLimit: RateLimitConfig = {
maxRequests: 50,
windowSeconds: 300, // 5 minutes
burst: 10,
keyPrefix: 'rl:custom',
}
const result = await rateLimiter.check('identifier', customLimit)
Redis vs In-Memory
The rate limiter automatically uses Redis if available, falling back to in-memory:
# .env.local
REDIS_URL=redis://localhost:6379
# OR
UPSTASH_REDIS_URL=rediss://...
Redis Benefits:
- Distributed rate limiting across multiple servers
- Persistent rate limits (survive server restarts)
- Better performance for high-traffic scenarios
- Atomic operations with Lua scripts
In-Memory Benefits:
- No external dependencies
- Zero latency
- Good for development and low-traffic scenarios
- Automatic cleanup
CSRF Protection
Double-Submit Cookie Pattern
The application uses the secure double-submit cookie pattern:
import { withCsrfProtection } from '@/lib/security/csrf'
export const POST = compose(
withErrorHandler,
withCsrfProtection, // Validates CSRF token
withAuth
)(async (request, context) => {
// CSRF token validated - safe to proceed
})
Getting CSRF Token
Client-side:
// 1. Get CSRF token
const response = await fetch('/api/csrf')
const { csrfToken, headerName } = await response.json()
// 2. Include in state-changing requests
await fetch('/api/protected', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
[headerName]: csrfToken, // Usually 'X-CSRF-Token'
},
body: JSON.stringify({ data: 'example' }),
})
Configuration
# .env.local
# CSRF secret (required in production, auto-generated in dev)
CSRF_SECRET=your-32-character-secret-here
# Skip CSRF validation in development (optional)
SKIP_CSRF=true
Token Lifecycle
- Generation: Random 32-byte token, HMAC-signed
- Storage: HTTP-only cookie (secure in production)
- Validation: Header token must match cookie token
- Expiry: 24 hours
- Refresh: Call
/api/csrfto get new token
IP Blocking
Automatic Abuse Detection
The IP blocker automatically blocks IPs based on configurable rules:
import { ipBlocker, DEFAULT_BLOCK_RULES } from '@/lib/security/ip-blocker'
// Automatically applied in middleware
// Triggers based on abuse patterns:
// - Failed login attempts
// - Rate limit violations
// - CSRF violations
// - SQL injection attempts
// - XSS attempts
Block Rules
DEFAULT_BLOCK_RULES = {
FAILED_LOGIN: {
threshold: 10, // 10 failed attempts
windowSeconds: 900, // in 15 minutes
blockDurationSeconds: 3600, // block for 1 hour
},
RATE_LIMIT_ABUSE: {
threshold: 50, // 50 rate limit hits
windowSeconds: 300, // in 5 minutes
blockDurationSeconds: 7200, // block for 2 hours
},
SQL_INJECTION: {
threshold: 1, // immediate block
windowSeconds: 60,
blockDurationSeconds: 0, // permanent
},
// ... more rules
}
Manual IP Management
import { ipBlocker } from '@/lib/security/ip-blocker'
// Block an IP
await ipBlocker.blockIP(
'192.168.1.1',
'Suspicious activity',
3600 // 1 hour (0 = permanent)
)
// Unblock an IP
await ipBlocker.unblockIP('192.168.1.1')
// Whitelist (never block)
await ipBlocker.addToWhitelist('192.168.1.100')
// Blacklist (always block)
await ipBlocker.addToBlacklist('192.168.1.200')
// Check if blocked
const blocked = await ipBlocker.isBlocked('192.168.1.1')
if (blocked) {
console.log(blocked.reason)
console.log(blocked.expiresAt)
}
// Get all blocked IPs
const allBlocked = await ipBlocker.getAllBlockedIPs()
Recording Abuse
import { recordAbuseFromRequest } from '@/lib/security/ip-blocker'
// In your API route
const wasBlocked = await recordAbuseFromRequest(
request,
'FAILED_LOGIN',
'medium' // severity: low | medium | high | critical
)
if (wasBlocked) {
// IP was automatically blocked due to threshold
console.log('IP blocked due to abuse')
}
Penalty Box
Temporary blocks for immediate abuse:
import { addToPenaltyBox, isInPenaltyBox, removeFromPenaltyBox } from '@/middleware/rate-limit'
// Add IP to penalty box (1 hour)
addToPenaltyBox('192.168.1.1', 3600)
// Check
if (isInPenaltyBox('192.168.1.1')) {
// Blocked temporarily
}
// Remove
removeFromPenaltyBox('192.168.1.1')
Response Headers
Rate Limit Headers
All rate-limited responses include:
X-RateLimit-Limit: 100 // Max requests in window
X-RateLimit-Remaining: 95 // Remaining requests
X-RateLimit-Reset: 1640000000 // Unix timestamp when limit resets
Retry-After: 60 // Seconds to wait (if rate limited)
Security Headers
Applied by middleware:
Content-Security-Policy: ...
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Error Responses
429 Too Many Requests
{
"error": "Too Many Requests",
"message": "Rate limit exceeded. Try again in 60 seconds.",
"code": "RATE_LIMIT_EXCEEDED",
"retryAfter": 60
}
403 Forbidden (IP Blocked)
{
"error": "Access Denied",
"message": "Your IP address has been blocked",
"code": "IP_BLOCKED",
"reason": "FAILED_LOGIN (10 violations)",
"blockedAt": "2024-01-01T00:00:00.000Z",
"retryAfter": 3600
}
403 Forbidden (CSRF)
{
"error": "Forbidden",
"message": "Invalid or missing CSRF token",
"code": "CSRF_VALIDATION_FAILED"
}
Best Practices
1. Layer Your Protection
// Use multiple layers of protection
export const POST = compose(
withErrorHandler, // Handle errors gracefully
withLogging, // Log all requests
withCsrfProtection, // Prevent CSRF attacks
withAuth, // Require authentication
withRateLimit(...) // Prevent abuse
)(async (request, context) => {
// Your secure handler
})
2. Use Appropriate Rate Limits
- Auth endpoints: Very strict (3-5/min)
- Read operations: Lenient (100/min)
- Write operations: Moderate (10-20/min)
- Expensive operations: Strict (5/min)
3. Provide Clear Error Messages
if (!result.allowed) {
return Response.json(
{
error: 'Too Many Requests',
message: `You've sent too many messages. Please wait ${result.retryAfter} seconds.`,
retryAfter: result.retryAfter,
helpUrl: 'https://docs.example.com/rate-limits',
},
{ status: 429 }
)
}
4. Monitor and Alert
// Log rate limit violations
if (!result.allowed) {
console.warn('[RateLimit]', {
identifier: 'user:123',
endpoint: '/api/messages',
limit: result.limit,
timestamp: new Date().toISOString(),
})
// Send to monitoring service
captureMessage('Rate limit exceeded', {
tags: { endpoint: '/api/messages' },
extra: { identifier: 'user:123', limit: result.limit },
})
}
5. Test Rate Limits
// __tests__/rate-limit.test.ts
import { clearAllRateLimits } from '@/middleware/rate-limit'
describe('Rate Limiting', () => {
beforeEach(() => {
clearAllRateLimits() // Clear between tests
})
it('should block after limit exceeded', async () => {
// Make 11 requests (limit is 10)
for (let i = 0; i < 11; i++) {
const response = await fetch('/api/test')
if (i < 10) {
expect(response.status).toBe(200)
} else {
expect(response.status).toBe(429)
}
}
})
})
Environment Variables
# Redis (optional, falls back to in-memory)
REDIS_URL=redis://localhost:6379
UPSTASH_REDIS_URL=rediss://...
# CSRF Protection
CSRF_SECRET=your-32-character-secret-minimum
SKIP_CSRF=true # Development only
# Skip validation during build
SKIP_ENV_VALIDATION=true
Monitoring
Rate Limit Status Endpoint
// GET /api/rate-limit-status
import { getRateLimitStatus } from '@/middleware/rate-limit'
export async function GET(request: NextRequest) {
const pathname = request.nextUrl.searchParams.get('path') || '/api/messages'
const status = getRateLimitStatus(request, pathname)
return Response.json({
limit: status.limit,
remaining: status.remaining,
reset: new Date(status.reset * 1000).toISOString(),
allowed: status.allowed,
})
}
Blocked IPs Dashboard
// GET /api/admin/blocked-ips
import { ipBlocker } from '@/lib/security/ip-blocker'
export async function GET() {
const blocked = await ipBlocker.getAllBlockedIPs()
const whitelist = await ipBlocker.getWhitelist()
const blacklist = await ipBlocker.getBlacklist()
return Response.json({
blocked: blocked.map((b) => ({
ip: b.ip,
reason: b.reason,
blockedAt: new Date(b.blockedAt).toISOString(),
expiresAt: b.expiresAt ? new Date(b.expiresAt).toISOString() : null,
type: b.blockType,
})),
whitelist,
blacklist,
stats: {
totalBlocked: blocked.length,
totalWhitelisted: whitelist.length,
totalBlacklisted: blacklist.length,
},
})
}
Example: Complete Protected Endpoint
See /src/app/api/example-protected/route.ts for a complete example implementing all protections.
Troubleshooting
Rate Limit Not Working
-
Check Redis connection:
# Test Redis connection redis-cli ping -
Check logs for errors:
# Look for [RateLimiter] errors -
Verify environment variables
False Positives
If legitimate users are being blocked:
- Adjust rate limit thresholds
- Add to whitelist:
await ipBlocker.addToWhitelist('trusted-ip') - Review block rules
Performance Issues
- Use Redis for distributed systems
- Adjust cleanup intervals
- Consider using edge middleware only
- Monitor Redis memory usage
Security Considerations
- Always use HTTPS in production - Required for secure cookies
- Set strong CSRF_SECRET - Minimum 32 characters
- Monitor rate limit violations - Alert on suspicious patterns
- Regularly review blocked IPs - Clean up expired blocks
- Test your limits - Ensure they work as expected
- Use whitelisting carefully - Only for trusted IPs
- Log security events - For audit and forensics
Migration Guide
From Basic to Advanced
// Before
export async function POST(request: NextRequest) {
// No protection
return Response.json({ success: true })
}
// After
export const POST = compose(
withErrorHandler,
withLogging,
withCsrfProtection,
withAuth,
withRateLimit({ limit: 10, window: 60 })
)(async (request: AuthenticatedRequest, context) => {
return successResponse({ success: true })
})