Skip to content

feat(rate-limiting): implement cost-aware per-endpoint rate limits with Redis support#697

Open
Clinton6801 wants to merge 2 commits into
Pulsefy:mainfrom
Clinton6801:feat/460-rate-limits-per-endpoint
Open

feat(rate-limiting): implement cost-aware per-endpoint rate limits with Redis support#697
Clinton6801 wants to merge 2 commits into
Pulsefy:mainfrom
Clinton6801:feat/460-rate-limits-per-endpoint

Conversation

@Clinton6801

@Clinton6801 Clinton6801 commented Jul 1, 2026

Copy link
Copy Markdown

Overview

Implements cost-aware, route-specific rate limiting for the Soter backend API using NestJS Throttler with Redis support for multi-instance deployments.
closes #460

Changes

🎯 New Files Created

  1. src/common/config/rate-limit.config.ts

    • Defines cost-aware rate limit tiers
    • OTP/email/phone: 20 req/min (strictest - external API calls)
    • Verification endpoints: 30 req/min (strict - document processing)
    • General API: 100 req/min (moderate - default)
    • Search: 50 req/min (moderate - read-only queries)
    • Health/docs: unlimited (monitoring requirements)
    • Environment variable overrides: THROTTLE_*_LIMIT, THROTTLE_*_TTL
  2. src/common/decorators/skip-throttle.decorator.ts

    • Metadata decorator for routes that should bypass rate limiting
    • Used on health checks, metrics, and docs endpoints
  3. src/common/guards/throttle.guard.ts

    • CostAwareThrottlerGuard extends NestJS ThrottlerGuard
    • Honors @SkipThrottle() decorator
    • Automatically exempts globally safe paths (health, metrics, docs)
    • Integrates with per-endpoint @Throttle() configurations
  4. test/rate-limit-cost-aware.e2e-spec.ts

    • Comprehensive E2E tests covering:
      • Strictest limits enforce on OTP endpoints
      • Strict limits enforce on verification endpoints
      • Moderate limits enforce on general endpoints
      • Health endpoints completely bypass rate limiting
      • Limits reset after TTL window
      • Different throttle groups have independent limits
      • Retry-After header present on 429 responses

📝 Modified Files

  1. src/app.module.ts

    • Configured ThrottlerModule.forRootAsync() with Redis support
    • Automatic Redis connection with fallback to in-memory storage
    • Reconnection strategy with exponential backoff (10 retries)
    • Added CostAwareThrottlerGuard to global providers
  2. src/health/health.controller.ts

    • Added @SkipThrottle() to all health endpoints:
      • GET /api/v1/health
      • GET /api/v1/health/live
      • GET /api/v1/health/ready
      • GET /api/v1/health/error
      • GET /api/v1/health/onchain
  3. src/verification/verification.controller.ts

    • Applied @Throttle('verify-otp', { limit: 20, ttl: 60 }) to OTP endpoints:
      • POST /api/v1/verification/start
      • POST /api/v1/verification/resend
      • POST /api/v1/verification/complete
    • Applied @Throttle('verify', { limit: 30, ttl: 60 }) to verification endpoints:
      • POST /api/v1/verification
      • POST /api/v1/verification/claims/:id/enqueue
    • Added @ApiTooManyRequestsResponse() to all decorated endpoints
  4. .env.example

    • Added rate limit configuration variables with defaults:
      • THROTTLE_VERIFY_OTP_LIMIT=20, THROTTLE_VERIFY_OTP_TTL=60
      • THROTTLE_VERIFY_LIMIT=30, THROTTLE_VERIFY_TTL=60
      • THROTTLE_GENERAL_LIMIT=100, THROTTLE_GENERAL_TTL=60
      • THROTTLE_SEARCH_LIMIT=50, THROTTLE_SEARCH_TTL=60

Key Features

Cost-Aware Limits: Different limits for different endpoint categories based on resource cost
Multi-Instance Compatible: Redis-backed rate limiting for distributed deployments
Graceful Fallback: In-memory storage if Redis unavailable (development only)
Route-Specific: Apply limits per-endpoint with @Throttle() decorator
Safe Exemptions: Monitoring endpoints (health, metrics, docs) never rate-limited
Standards Compliant: Returns 429 with Retry-After header per HTTP standards
Well Documented: Swagger integration with @ApiTooManyRequestsResponse()
Comprehensive Tests: Full E2E test coverage for all scenarios

Architecture

Multi-Layer Approach:

  1. Express Middleware (in-memory global fallback)
  2. NestJS Guards (in execution order):
    • ApiKeyGuard → RolesGuard → ScopesGuard → AdaptiveRateLimitGuard → CostAwareThrottlerGuard
  3. Route decorators (@Throttle(), @SkipThrottle())

Redis Support:

  • Shared counters across instances
  • Automatic reconnection with backoff
  • Configurable via REDIS_HOST, REDIS_PORT

Response Format

429 Too Many Requests

{
  "statusCode": 429,
  "message": "Too many requests, please try again later.",
  "error": "Too Many Requests"
}
Headers:

Retry-After: 45
RateLimit-Limit: 30
RateLimit-Remaining: 0
RateLimit-Reset: 45
Testing
Run Tests:

npm run test:e2e -- rate-limit-cost-aware.e2e-spec.ts
Test Scenarios Covered:

Rate limit enforcement at each tier
Health endpoint exemption
TTL window reset behavior
Independent limit groups
Retry-After header validation
Configuration
Development (.env):

REDIS_HOST=localhost
REDIS_PORT=6379
THROTTLE_VERIFY_OTP_LIMIT=20
THROTTLE_VERIFY_LIMIT=30
THROTTLE_GENERAL_LIMIT=100
THROTTLE_SEARCH_LIMIT=50
All values configurable per environment.

Breaking Changes
None. This is backward compatible.

Migration Notes
No code changes required for existing endpoints
Health endpoints now properly exempted (improvement)
Verification endpoints now have stricter limits (security improvement)
Environment variables optional (sensible defaults provided)
Issue
closes #460 

Checklist
✅ Tests pass: npm run test:e2e
✅ No compilation errors
✅ Rate limits properly enforced at each tier
✅ Health endpoints bypass rate limiting
✅ Redis connection works with fallback
✅ Multi-instance deployments supported
✅ Swagger documentation includes 429 responses
✅ Environment variables documented in .env.example

…th Redis support

- Configure route-specific rate limits based on endpoint cost:
  * OTP/email/phone: strictest (20 req/min) - high cost external API calls
  * Verification endpoints: strict (30 req/min) - document processing
  * General API endpoints: moderate (100 req/min) - standard CRUD
  * Search endpoints: moderate (50 req/min) - read-only queries
  * Health/docs: no rate limiting - monitoring requirements

- Implement Redis-backed ThrottlerModule for multi-instance compatibility
  * Graceful fallback to in-memory storage if Redis unavailable
  * Automatic reconnection with exponential backoff
  * Configurable via environment variables (THROTTLE_*_LIMIT, THROTTLE_*_TTL)

- Add @SkipThrottle() decorator for routes that should bypass rate limiting
  * Applied to health checks, metrics, docs endpoints
  * Respects globally exempt paths via regex patterns

- Create CostAwareThrottlerGuard extending NestJS ThrottlerGuard
  * Honors @SkipThrottle() decorator
  * Automatically exempts health/metrics/docs paths
  * Works with per-endpoint @Throttle() configurations

- Apply rate limits to verification endpoints:
  * POST /api/v1/verification/start - @Throttle('verify-otp')
  * POST /api/v1/verification/resend - @Throttle('verify-otp')
  * POST /api/v1/verification/complete - @Throttle('verify-otp')
  * POST /api/v1/verification - @Throttle('verify')
  * POST /api/v1/verification/claims/:id/enqueue - @Throttle('verify')

- Skip rate limiting on health endpoints:
  * GET /api/v1/health - @SkipThrottle()
  * GET /api/v1/health/live - @SkipThrottle()
  * GET /api/v1/health/ready - @SkipThrottle()
  * GET /api/v1/health/error - @SkipThrottle()
  * GET /api/v1/health/onchain - @SkipThrottle()

- Add comprehensive E2E tests validating:
  * Strictest limits enforce on OTP endpoints (N+1 request returns 429)
  * Strict limits enforce on verification endpoints
  * Moderate limits enforce on general endpoints
  * Health endpoints completely bypass rate limiting
  * Docs endpoints completely bypass rate limiting
  * Limits reset after TTL window expires
  * Different throttle groups have independent limits
  * Retry-After header present on 429 responses

- Update environment configuration (.env.example):
  * THROTTLE_VERIFY_OTP_LIMIT, THROTTLE_VERIFY_OTP_TTL
  * THROTTLE_VERIFY_LIMIT, THROTTLE_VERIFY_TTL
  * THROTTLE_GENERAL_LIMIT, THROTTLE_GENERAL_TTL
  * THROTTLE_SEARCH_LIMIT, THROTTLE_SEARCH_TTL
  * All with sensible defaults for production use

- Add API documentation:
  * @ApiTooManyRequestsResponse() on rate-limited endpoints
  * Swagger shows 429 responses with proper descriptions

Returns 429 Too Many Requests with Retry-After header when limits exceeded.
Multi-instance deployments use Redis for shared rate limit counters.
In-memory fallback available for local development.

Closes Pulsefy#460
@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

@Alu-card19 is attempting to deploy a commit to the Cedarich's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Jul 1, 2026

Copy link
Copy Markdown

@Clinton6801 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rate Limits per Endpoint (Cost-Aware)

2 participants