Overview
Ceboelha API implements comprehensive rate limiting to prevent abuse and ensure fair usage:
- Per-IP tracking: Rate limits are enforced per client IP address
- Endpoint-specific limits: Different endpoints have different thresholds
- In-memory storage: Fast, low-latency rate limit checks (upgradeable to Redis)
- Automatic cleanup: Expired entries are removed every minute
- Response headers: Clients receive limit information in headers
Rate Limit Tiers
General API Limit
Default for most endpoints:
- 100 requests per minute per IP
- Applied to: Diary entries, food searches, profile updates, etc.
// rate-limiter.middleware.ts:151-155
export const generalRateLimiter = createRateLimiter({
windowMs: env.RATE_LIMIT_WINDOW, // 60000ms (1 minute)
maxRequests: env.RATE_LIMIT_MAX, // 100 requests
message: 'Muitas requisições. Aguarde um momento e tente novamente.',
})
Configuration (env.ts:50-52):
RATE_LIMIT_MAX=100
RATE_LIMIT_WINDOW=60000 # 1 minute in milliseconds
Authentication Endpoints (Stricter)
For login/register endpoints:
- 5 requests per 15 minutes per IP
- Prevents brute force attacks
- Applied to:
/auth/register, /auth/login, /auth/refresh
// rate-limiter.middleware.ts:161-165
export const authRateLimiter = createRateLimiter({
windowMs: env.AUTH_RATE_LIMIT_WINDOW, // 900000ms (15 minutes)
maxRequests: env.AUTH_RATE_LIMIT_MAX, // 5 requests
message: 'Muitas tentativas de login. Aguarde 15 minutos antes de tentar novamente.',
})
Configuration (env.ts:54-56):
AUTH_RATE_LIMIT_MAX=5
AUTH_RATE_LIMIT_WINDOW=900000 # 15 minutes in milliseconds
Sensitive Operations (Most Strict)
For password changes, account deletion, session revocation:
- 3 requests per 5 minutes per IP
- Applied to:
/auth/sessions/:id (DELETE), /users/change-password, etc.
// rate-limiter.middleware.ts:171-175
export const sensitiveRateLimiter = createRateLimiter({
windowMs: 300000, // 5 minutes
maxRequests: 3,
message: 'Muitas tentativas. Aguarde alguns minutos antes de tentar novamente.',
})
Global IP Limit
Across ALL endpoints:
- 200 requests per minute per IP
- Prevents total API abuse from a single source
// rate-limiter.middleware.ts:185-209
export function createGlobalRateLimiter() {
return new Elysia({ name: 'global-rate-limiter' }).derive(
{ as: 'scoped' },
({ request }) => {
const ip = /* extract IP */
const key = `global:${ip}`
const windowMs = 60000 // 1 minute
const maxRequests = 200 // 200 requests total
const entry = globalStore.increment(key, windowMs)
if (entry.count > maxRequests) {
throw new RateLimitError(
'Limite de requisições excedido. Tente novamente em breve.'
)
}
}
)
}
Admin Endpoints
For admin-only operations:
Read operations:
- 30 requests per minute per IP
Write operations (create/update/delete):
- 10 requests per minute per IP
// rate-limiter.middleware.ts:222-236
export const adminRateLimiter = createRateLimiter({
windowMs: 60000,
maxRequests: 30,
message: 'Limite de requisições admin excedido. Aguarde antes de continuar.',
})
export const adminWriteRateLimiter = createRateLimiter({
windowMs: 60000,
maxRequests: 10,
message: 'Muitas operações de escrita. Aguarde antes de continuar.',
})
Every response includes rate limit information:
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1705320600000
Maximum number of requests allowed in the current window
Number of requests remaining in the current window
Unix timestamp (milliseconds) when the rate limit window resets
Implementation (rate-limiter.middleware.ts:133-140):
.onAfterHandle(({ set, rateLimitInfo }) => {
if (rateLimitInfo) {
set.headers['X-RateLimit-Limit'] = String(rateLimitInfo.limit)
set.headers['X-RateLimit-Remaining'] = String(rateLimitInfo.remaining)
set.headers['X-RateLimit-Reset'] = String(rateLimitInfo.reset)
}
})
Rate Limit Exceeded Response
When a rate limit is exceeded, you’ll receive a 429 Too Many Requests response:
{
"success": false,
"error": "RateLimitError",
"code": "RATE_LIMIT",
"message": "Muitas requisições. Aguarde um momento e tente novamente."
}
The custom error class (errors/index.ts:52-57):
export class RateLimitError extends AppError {
constructor(message: string = 'Muitas requisições. Tente novamente mais tarde.') {
super(message, 429, 'RATE_LIMIT')
this.name = 'RateLimitError'
}
}
How It Works
The client IP is extracted from request headers with fallbacks:
// rate-limiter.middleware.ts:103-106
const ip =
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
request.headers.get('x-real-ip') ||
'unknown'
- First tries
X-Forwarded-For header (from proxies/load balancers)
- Falls back to
X-Real-IP header
- Defaults to
'unknown' if neither exists
If you’re behind a proxy (Nginx, Cloudflare, etc.), make sure it sets the X-Forwarded-For header correctly.
Rate Limit Key
Rate limits are tracked per IP and path:
// rate-limiter.middleware.ts:108-110
const path = new URL(request.url).pathname
const key = `${ip}:${path}`
This means:
192.168.1.100:/auth/login has a separate limit from
192.168.1.100:/diary/entries
In-Memory Store
The rate limiter uses an in-memory Map for fast lookups:
// rate-limiter.middleware.ts:34-85
class RateLimitStore {
private store: Map<string, RateLimitEntry> = new Map()
private cleanupInterval: Timer
increment(key: string, windowMs: number): RateLimitEntry {
const now = Date.now()
const existing = this.get(key)
if (existing) {
existing.count++
return existing
}
const entry: RateLimitEntry = {
count: 1,
resetAt: now + windowMs,
}
this.store.set(key, entry)
return entry
}
private cleanup() {
const now = Date.now()
for (const [key, entry] of this.store.entries()) {
if (now > entry.resetAt) {
this.store.delete(key)
}
}
}
}
Automatic cleanup runs every minute to remove expired entries (rate-limiter.middleware.ts:40).
The in-memory store is not shared across server instances. For production with multiple servers, consider upgrading to Redis.
Window Reset
Rate limit windows use sliding windows:
- First request at
10:00:00 starts a window that expires at 10:01:00
- Subsequent requests within that minute increment the counter
- After
10:01:00, the counter resets to 0
- A new window starts on the next request
// rate-limiter.middleware.ts:56-69
increment(key: string, windowMs: number): RateLimitEntry {
const now = Date.now()
const existing = this.get(key)
if (existing) {
existing.count++ // Increment within window
return existing
}
// Create new window
const entry: RateLimitEntry = {
count: 1,
resetAt: now + windowMs,
}
this.store.set(key, entry)
return entry
}
Custom Rate Limiting
You can create custom rate limiters for specific endpoints:
import { createRateLimiter } from '@/shared/middlewares/rate-limiter.middleware'
// Custom limit: 10 requests per 30 seconds
const customLimiter = createRateLimiter({
windowMs: 30000, // 30 seconds
maxRequests: 10, // 10 requests
message: 'Custom rate limit exceeded',
})
// Apply to route
export const myController = new Elysia()
.use(customLimiter)
.get('/custom-endpoint', async () => {
return { success: true }
})
Best Practices
For API Clients
-
Check response headers before making requests:
const remaining = response.headers.get('X-RateLimit-Remaining')
if (remaining < 10) {
console.warn('Approaching rate limit!')
}
-
Implement exponential backoff when receiving 429 errors:
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url)
if (response.status === 429) {
const resetTime = response.headers.get('X-RateLimit-Reset')
const waitTime = Math.max(1000, resetTime - Date.now())
await sleep(waitTime * (i + 1)) // Exponential backoff
continue
}
return response
}
}
-
Batch requests when possible instead of making many small requests
For Server Administrators
- Monitor rate limit hits in logs to detect abuse patterns
- Adjust limits via environment variables based on usage:
# More generous for trusted environments
RATE_LIMIT_MAX=200
AUTH_RATE_LIMIT_MAX=10
- Consider Redis for production multi-server deployments:
// Example Redis-backed rate limiter
class RedisRateLimitStore {
async increment(key: string, windowMs: number) {
const count = await redis.incr(key)
if (count === 1) {
await redis.expire(key, Math.ceil(windowMs / 1000))
}
return count
}
}
Bypassing Rate Limits (Development)
Rate limits apply in all environments. For local development testing:
Option 1: Increase limits
# .env.local
RATE_LIMIT_MAX=1000
AUTH_RATE_LIMIT_MAX=100
Option 2: Clear rate limit state
// In development tools
import { globalStore } from '@/shared/middlewares/rate-limiter.middleware'
// Clear all rate limits (destructive!)
globalStore.destroy()
Never disable rate limiting in production. It’s a critical security feature.
Comparison with Account Lockout
Rate limiting and account lockout work together but serve different purposes:
| Feature | Rate Limiting | Account Lockout |
|---|
| Scope | Per IP + endpoint | Per email address |
| Purpose | Prevent API abuse | Prevent brute force login |
| Duration | Short windows (1-15 min) | Longer duration (15 min) |
| Reset | Automatic after window | After successful login |
| Tracked in | Memory (Map) | Database (MongoDB) |
See Authentication for account lockout details.