BillTracker/docs/RATE_LIMITING_ENHANCEMENT.md

3.8 KiB

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

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

  • 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)