Rate Limiting Enhancement — Future Consideration
Date: 2026-05-08
Status: Documented for future implementation
Priority: Low (current per-IP limits are functional for self-hosted use)
Current Implementation
All rate limiters in middleware/rateLimiter.js use per-IP limiting:
keyGenerator: (req) => req.ip
Current Limits
| Limiter |
Limit |
Keyed By |
importLimiter |
20 per 15 min |
IP |
exportLimiter |
30 per 15 min |
IP |
adminActionLimiter |
30 per 15 min |
IP |
demoDataLimiter |
3 per 15 min |
IP |
backupOperationLimiter |
5 per hour |
IP |
loginLimiter |
5 per 15 min |
IP |
passwordLimiter |
3 per hour |
IP |
oidcLimiter |
10 per 15 min |
IP |
Limitations of Per-IP Limiting
| Scenario |
Problem |
| Shared office network |
All users share one rate limit bucket |
| VPN users |
Multiple users behind same IP share limit |
| Same user, multiple devices |
Separate limits per device (may be desired) |
| Malicious actor with IP rotation |
Can bypass limits by rotating IPs |
Recommended Enhancement: Hybrid Per-User + Per-IP
Implementation
// middleware/rateLimiter.js
const hybridKeyGenerator = (req) => {
// Authenticated users get per-user limits
if (req.user?.id) {
return `user:${req.user.id}`;
}
// Anonymous requests get per-IP limits
return `ip:${req.ip}`;
};
// Apply to existing limiters
const importLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 20,
keyGenerator: hybridKeyGenerator, // <-- Changed
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
res.status(429).json({
error: 'Too many import requests. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
retryAfter: Math.ceil(req.rateLimit.resetTime / 1000),
});
},
});
Benefits
| Benefit |
Explanation |
| Fair per-user allocation |
Each user gets their own rate limit bucket |
| Shared network friendly |
Office/VPN users don't share limits |
| Abuse resistant |
Can't bypass by IP rotation if authenticated |
| Backwards compatible |
Falls back to IP for anonymous requests |
Alternative: Separate Limits for Auth vs Anonymous
const createHybridLimiter = (authMax, anonMax, windowMs) => {
const authLimiter = rateLimit({
windowMs,
max: authMax,
keyGenerator: (req) => req.user?.id ? `user:${req.user.id}` : 'anonymous',
skip: (req) => !req.user?.id, // Skip if not authenticated
});
const anonLimiter = rateLimit({
windowMs,
max: anonMax,
keyGenerator: (req) => `ip:${req.ip}`,
skip: (req) => !!req.user?.id, // Skip if authenticated
});
return (req, res, next) => {
if (req.user?.id) {
return authLimiter(req, res, next);
}
return anonLimiter(req, res, next);
};
};
// Usage: authenticated users get 50/15min, anonymous get 20/15min
const importLimiter = createHybridLimiter(50, 20, 15 * 60 * 1000);
When to Implement
| Trigger |
Action |
| Multi-tenant deployment |
Higher priority — many users sharing infrastructure |
| API key authentication |
Required — API keys need per-key limits |
| Public cloud hosting |
Recommended — shared IPs common |
| Self-hosted, small team |
Low priority — current per-IP is adequate |
Related Files
middleware/rateLimiter.js — Current rate limiter definitions
server.js — Rate limiter application (routes)
docs/SECURITY.md — Security documentation
Notes
- Current per-IP limits are appropriate for self-hosted SQLite deployment
- Hybrid approach should be considered if adding API keys or multi-tenant support
- Rate limit storage is in-memory (Redis not required for current scale)