4.8 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Enable Rate Limiting for API Protection | MEDIUM | Prevents abuse, brute-force attacks, and DoS | production, security, rate-limiting, abuse-prevention |
Enable Rate Limiting for API Protection
PocketBase v0.23+ includes built-in rate limiting. Enable it to protect against brute-force attacks, API abuse, and excessive resource consumption.
v0.36.7 behavioral change: the built-in limiter switched from a sliding-window to a fixed-window strategy. This is cheaper and more predictable, but it means a client can send
2 * maxRequestsin quick succession if they straddle the window boundary. Size your limits with that worst case in mind, and put Nginx/Caddy rate limiting in front of PocketBase for defense in depth (see examples below).
Incorrect (no rate limiting):
# Running without rate limiting
./pocketbase serve
# Vulnerable to:
# - Brute-force password attacks
# - API abuse and scraping
# - DoS from excessive requests
# - Account enumeration attempts
Correct (enable rate limiting):
# Enable via command line flag
./pocketbase serve --rateLimiter=true
# Or configure specific limits (requests per second per IP)
./pocketbase serve --rateLimiter=true --rateLimiterRPS=10
Configure via Admin Dashboard:
Navigate to Settings > Rate Limiter:
- Enable rate limiter: Toggle on
- Max requests/second: Default 10, adjust based on needs
- Exempt endpoints: Optionally whitelist certain paths
Configure programmatically (Go/JS hooks):
// In pb_hooks/rate_limit.pb.js
routerAdd("GET", "/api/public/*", (e) => {
// Custom rate limit for specific endpoints
}, $apis.rateLimit(100, "10s")); // 100 requests per 10 seconds
// Stricter limit for auth endpoints
routerAdd("POST", "/api/collections/users/auth-*", (e) => {
// Auth endpoints need stricter limits
}, $apis.rateLimit(5, "1m")); // 5 attempts per minute
Rate limiting with reverse proxy (additional layer):
# Nginx rate limiting (defense in depth)
http {
# Define rate limit zones
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=auth:10m rate=1r/s;
server {
# General API rate limit
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://pocketbase;
}
# Strict limit for auth endpoints
location /api/collections/users/auth {
limit_req zone=auth burst=5 nodelay;
proxy_pass http://pocketbase;
}
# Stricter limit for superuser auth
location /api/collections/_superusers/auth {
limit_req zone=auth burst=3 nodelay;
proxy_pass http://pocketbase;
}
}
}
# Caddy with rate limiting plugin
myapp.com {
rate_limit {
zone api {
key {remote_host}
events 100
window 10s
}
zone auth {
key {remote_host}
events 5
window 1m
}
}
@auth path /api/collections/*/auth*
handle @auth {
rate_limit { zone auth }
reverse_proxy 127.0.0.1:8090
}
handle {
rate_limit { zone api }
reverse_proxy 127.0.0.1:8090
}
}
Handle rate limit errors in client:
async function makeRequest(fn, retries = 0, maxRetries = 3) {
try {
return await fn();
} catch (error) {
if (error.status === 429 && retries < maxRetries) {
// Rate limited - wait and retry with limit
const retryAfter = error.response?.retryAfter || 60;
console.log(`Rate limited. Retry ${retries + 1}/${maxRetries} after ${retryAfter}s`);
// Show user-friendly message
showMessage('Too many requests. Please wait a moment.');
await sleep(retryAfter * 1000);
return makeRequest(fn, retries + 1, maxRetries);
}
throw error;
}
}
// Usage
const result = await makeRequest(() =>
pb.collection('posts').getList(1, 20)
);
Recommended limits by endpoint type:
| Endpoint Type | Suggested Limit | Reason |
|---|---|---|
| Auth endpoints | 5-10/min | Prevent brute-force |
| Password reset | 3/hour | Prevent enumeration |
| Record creation | 30/min | Prevent spam |
| General API | 60-100/min | Normal usage |
| Public read | 100-200/min | Higher for reads |
| File uploads | 10/min | Resource-intensive |
Monitoring rate limit hits:
// Check PocketBase logs for rate limit events
// Or set up alerting in your monitoring system
// Client-side tracking
pb.afterSend = function(response, data) {
if (response.status === 429) {
trackEvent('rate_limit_hit', {
endpoint: response.url,
timestamp: new Date()
});
}
return data;
};
Reference: PocketBase Going to Production