-
Notifications
You must be signed in to change notification settings - Fork 0
Rate Limiting API Protection
Complete documentation for the nself-chat rate limiting and API protection system.
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
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 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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
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
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/hourimport { 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...
}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 })
})const customLimit: RateLimitConfig = {
maxRequests: 50,
windowSeconds: 300, // 5 minutes
burst: 10,
keyPrefix: 'rl:custom',
}
const result = await rateLimiter.check('identifier', customLimit)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
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
})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' }),
})# .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- 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
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 attemptsDEFAULT_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
}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()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')
}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')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)
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": "Too Many Requests",
"message": "Rate limit exceeded. Try again in 60 seconds.",
"code": "RATE_LIMIT_EXCEEDED",
"retryAfter": 60
}{
"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
}{
"error": "Forbidden",
"message": "Invalid or missing CSRF token",
"code": "CSRF_VALIDATION_FAILED"
}// 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
})- Auth endpoints: Very strict (3-5/min)
- Read operations: Lenient (100/min)
- Write operations: Moderate (10-20/min)
- Expensive operations: Strict (5/min)
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 }
)
}// 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 },
})
}// __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)
}
}
})
})# 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// 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,
})
}// 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,
},
})
}See /src/app/api/example-protected/route.ts for a complete example implementing all protections.
-
Check Redis connection:
# Test Redis connection redis-cli ping -
Check logs for errors:
# Look for [RateLimiter] errors -
Verify environment variables
If legitimate users are being blocked:
- Adjust rate limit thresholds
- Add to whitelist:
await ipBlocker.addToWhitelist('trusted-ip')
- Review block rules
- Use Redis for distributed systems
- Adjust cleanup intervals
- Consider using edge middleware only
- Monitor Redis memory usage
- 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
// 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 })
})nself-chat v0.3.0 | GitHub | Issues | Discussions | Demo
Edit this page | MIT License | Β© 2026
(See π Security section below for 2FA, PIN Lock, and security audits.)
(Search lives in π Reference below.)
- π¬ Advanced Messaging
- π E2EE Setup
- π Search Setup
- π Call Management
- πΊ Live Streaming
- π₯οΈ Screen Sharing
- πΉ Video Calling
- ποΈ Voice Calling
- π± Mobile Optimization
- π§ͺ Testing
- π i18n
- π API Overview
- π Complete Reference
- π» API Examples
- π€ Bot API
- π Auth API
- π GraphQL Schema
- π Deployment Overview
- π³ Docker
- βΈοΈ Kubernetes
- β Helm Charts
- β Production Checklist
- π Production Validation
- π’ Multi-Tenant
- ποΈ Architecture
- π Diagrams
- ποΈ Database Schema
- π Project Structure
- π TypeScript Types
- π SPORT Reference
- π 2FA
- π¬ Messaging
- π Call Management
- π Call State Machine
- π E2EE
- πΊ Live Streaming
- π± Mobile Calls
- π PIN Lock
- π Polls
- π₯οΈ Screen Sharing
- π Search
- π Social Media
- ποΈ Voice Calling
- π Security Overview
- π‘οΈ Security Audit
- β‘ Performance
- π Best Practices
- π 2FA
- π PIN Lock
- π E2EE
- π‘οΈ E2EE Audit
v1.0.0 β’ 2026