# API Implementation Plan - Section 4: Apply Rate Limiting and Throttling Rules **Date:** 2025-01-XX **Status:** Planning **Priority:** High **Related Document:** `API-ENDPOINTS-ANALYSIS.md` --- ## Executive Summary This document outlines the implementation plan for **Section 4: Apply Rate Limiting and Throttling Rules** across the IGNY8 API layer. The goal is to prevent abuse, ensure fair resource usage, and enforce plan limits by introducing consistent per-user and per-function rate limits using DRF throttling and internal usage tracking. **Key Objectives:** - Enable DRF throttling system with scoped rate limits - Assign throttle scopes to all major functions (AI, auth, content, images) - Implement plan-based throttling (optional, future phase) - Log and expose rate limit information - Integrate with existing credit system for quota enforcement --- ## Table of Contents 1. [Current State Analysis](#current-state-analysis) 2. [Implementation Tasks](#implementation-tasks) 3. [Task 1: Enable DRF Throttling System](#task-1-enable-drf-throttling-system) 4. [Task 2: Assign Throttle Scopes in ViewSets](#task-2-assign-throttle-scopes-in-viewsets) 5. [Task 3: Override User/Account-Level Throttles (Optional)](#task-3-override-useraccount-level-throttles-optional) 6. [Task 4: Log + Expose Rate Limit Info](#task-4-log--expose-rate-limit-info) 7. [Task 5: Advanced Daily/Monthly Quotas Per Plan](#task-5-advanced-dailymonthly-quotas-per-plan) 8. [Task 6: Testing Matrix](#task-6-testing-matrix) 9. [Task 7: Changelog Entry](#task-7-changelog-entry) 10. [Testing Strategy](#testing-strategy) 11. [Rollout Plan](#rollout-plan) 12. [Success Criteria](#success-criteria) --- ## Current State Analysis ### Current Rate Limiting Status Based on `API-ENDPOINTS-ANALYSIS.md`: 1. **Rate Limiting:** ❌ **Not Implemented** - All endpoints are currently unlimited - No throttling configuration in settings - No rate limit enforcement 2. **Credit System:** - `CreditUsageLog` model exists for tracking usage - `CreditBalance` model tracks account credits - Credit deduction happens but no rate limiting based on credits 3. **Plan System:** - `Plan` model exists with different tiers - Plans have credit limits but no API rate limits defined - No plan-based throttling implemented 4. **AI Functions:** - High-cost operations (AI generation, image generation) - Currently no rate limiting on these expensive endpoints - Risk of abuse and cost overruns --- ## Implementation Tasks ### Overview | Task ID | Task Name | Priority | Estimated Effort | Dependencies | |---------|-----------|----------|------------------|--------------| | 1.1 | Configure DRF throttling in settings | High | 2 hours | None | | 1.2 | Add debug throttle bypass | High | 1 hour | 1.1 | | 2.1 | Audit endpoints for throttle scopes | Medium | 3 hours | None | | 2.2 | Assign throttle scopes to ViewSets | High | 6 hours | 1.1 | | 3.1 | Create plan-based throttle class (optional) | Low | 8 hours | 1.1 | | 4.1 | Verify throttle headers | High | 2 hours | 2.2 | | 4.2 | Add frontend rate limit handling | Medium | 4 hours | 4.1 | | 4.3 | Implement abuse detection logging | Medium | 3 hours | 2.2 | | 5.1 | Integrate with credit system | Low | 6 hours | 3.1 | | 6.1 | Create test scenarios | High | 4 hours | 2.2 | | 7.1 | Create changelog entry | Low | 1 hour | All tasks | **Total Estimated Effort:** ~40 hours --- ## Task 1: Enable DRF Throttling System ### Goal Configure Django REST Framework to use scoped rate throttling with appropriate rate limits for different endpoint types. ### Implementation Steps #### Step 1.1: Configure DRF Throttling in Settings **File:** `backend/igny8_core/settings.py` **Update REST_FRAMEWORK configuration:** ```python REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'igny8_core.api.pagination.CustomPageNumberPagination', 'PAGE_SIZE': 10, 'DEFAULT_FILTER_BACKENDS': [ 'django_filters.rest_framework.DjangoFilterBackend', 'rest_framework.filters.SearchFilter', 'rest_framework.filters.OrderingFilter', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.AllowAny', # Will be changed in Section 2 ], 'DEFAULT_AUTHENTICATION_CLASSES': [ 'igny8_core.api.authentication.JWTAuthentication', 'igny8_core.api.authentication.CSRFExemptSessionAuthentication', 'rest_framework.authentication.BasicAuthentication', ], 'EXCEPTION_HANDLER': 'igny8_core.api.exception_handlers.custom_exception_handler', # Throttling Configuration 'DEFAULT_THROTTLE_CLASSES': [ 'rest_framework.throttling.ScopedRateThrottle', ], 'DEFAULT_THROTTLE_RATES': { # AI Functions - Expensive operations 'ai_function': '10/min', # AI content generation, clustering 'image_gen': '15/min', # Image generation # Content Operations 'content_write': '30/min', # Content creation, updates 'content_read': '100/min', # Content listing, retrieval # Authentication 'auth': '20/min', # Login, register, password reset 'auth_strict': '5/min', # Sensitive auth operations # Planner Operations 'planner': '60/min', # Keyword, cluster, idea operations 'planner_ai': '10/min', # AI-powered planner operations # Writer Operations 'writer': '60/min', # Task, content management 'writer_ai': '10/min', # AI-powered writer operations # System Operations 'system': '100/min', # Settings, prompts, profiles 'system_admin': '30/min', # Admin-only system operations # Billing Operations 'billing': '30/min', # Credit queries, usage logs 'billing_admin': '10/min', # Credit management (admin) # Default fallback 'default': '100/min', # Default for endpoints without scope }, } ``` #### Step 1.2: Add Debug Throttle Bypass **File:** `backend/igny8_core/settings.py` **Add environment variable:** ```python import os # Throttling Configuration IGNY8_DEBUG_THROTTLE = os.getenv('IGNY8_DEBUG_THROTTLE', 'False').lower() == 'true' # Only apply throttling if not in debug mode or explicitly enabled if not DEBUG and not IGNY8_DEBUG_THROTTLE: REST_FRAMEWORK['DEFAULT_THROTTLE_CLASSES'] = [ 'rest_framework.throttling.ScopedRateThrottle', ] else: # In debug mode, use a no-op throttle class REST_FRAMEWORK['DEFAULT_THROTTLE_CLASSES'] = [] ``` **Create Custom Throttle Class with Debug Bypass:** **File:** `backend/igny8_core/api/throttles.py` **Implementation:** ```python """ Custom Throttle Classes for IGNY8 API """ from django.conf import settings from rest_framework.throttling import ScopedRateThrottle class DebugScopedRateThrottle(ScopedRateThrottle): """ Scoped rate throttle that can be bypassed in debug mode. Usage: throttle_scope = 'ai_function' """ def allow_request(self, request, view): """ Check if request should be throttled. Bypass throttling if DEBUG=True or IGNY8_DEBUG_THROTTLE=True. """ # Bypass throttling in debug mode if settings.DEBUG or getattr(settings, 'IGNY8_DEBUG_THROTTLE', False): return True # Use parent class throttling logic return super().allow_request(request, view) ``` **Update settings to use custom throttle:** ```python 'DEFAULT_THROTTLE_CLASSES': [ 'igny8_core.api.throttles.DebugScopedRateThrottle', ], ``` **Estimated Time:** 2 hours (settings) + 1 hour (debug bypass) = 3 hours --- ## Task 2: Assign Throttle Scopes in ViewSets ### Goal Assign appropriate throttle scopes to all ViewSets and custom actions based on their function and resource cost. ### Implementation Steps #### Step 2.1: Audit Endpoints for Throttle Scopes **Action Items:** 1. List all ViewSets and custom actions 2. Categorize by function type (AI, content, auth, etc.) 3. Identify high-cost operations (AI, image generation) 4. Create mapping of endpoints to throttle scopes **Endpoint Categories:** **AI Functions (10/min):** - `POST /api/v1/planner/keywords/auto_cluster/` - AI clustering - `POST /api/v1/planner/clusters/auto_generate_ideas/` - AI idea generation - `POST /api/v1/writer/tasks/auto_generate_content/` - AI content generation - `POST /api/v1/writer/content/generate_image_prompts/` - AI prompt generation **Image Generation (15/min):** - `POST /api/v1/writer/images/generate_images/` - Image generation - `POST /api/v1/writer/images/auto_generate/` - Legacy image generation **Content Write (30/min):** - `POST /api/v1/writer/tasks/` - Create task - `POST /api/v1/writer/content/` - Create content - `PUT /api/v1/writer/tasks/{id}/` - Update task - `PUT /api/v1/writer/content/{id}/` - Update content **Planner AI (10/min):** - `POST /api/v1/planner/keywords/auto_cluster/` - `POST /api/v1/planner/clusters/auto_generate_ideas/` **Planner Standard (60/min):** - `GET /api/v1/planner/keywords/` - List keywords - `POST /api/v1/planner/keywords/` - Create keyword - `GET /api/v1/planner/clusters/` - List clusters - `POST /api/v1/planner/ideas/` - Create idea **Auth (20/min):** - `POST /api/v1/auth/login/` - `POST /api/v1/auth/register/` - `POST /api/v1/auth/change-password/` **Auth Strict (5/min):** - `POST /api/v1/auth/password-reset/` - If exists - `POST /api/v1/auth/users/invite/` - User invitation **System Admin (30/min):** - `POST /api/v1/system/settings/integrations/{pk}/save/` - `POST /api/v1/system/settings/integrations/{pk}/test/` **Billing Admin (10/min):** - `POST /api/v1/billing/credits/transactions/` - Create transaction **Estimated Time:** 3 hours #### Step 2.2: Assign Throttle Scopes to ViewSets **Example Implementation:** **Planner Module:** **File:** `backend/planner/api/viewsets.py` ```python from igny8_core.api.viewsets import BaseTenantViewSet class KeywordViewSet(BaseTenantViewSet): throttle_scope = 'planner' # Default for standard operations queryset = Keyword.objects.all() serializer_class = KeywordSerializer @action( detail=False, methods=['post'], throttle_scope='ai_function' # Override for AI operations ) def auto_cluster(self, request): # AI clustering logic pass class ClusterViewSet(BaseTenantViewSet): throttle_scope = 'planner' queryset = Cluster.objects.all() serializer_class = ClusterSerializer @action( detail=False, methods=['post'], throttle_scope='planner_ai' # AI-powered operations ) def auto_generate_ideas(self, request): # AI idea generation logic pass ``` **Writer Module:** **File:** `backend/writer/api/viewsets.py` ```python class TasksViewSet(BaseTenantViewSet): throttle_scope = 'writer' # Default for standard operations queryset = Task.objects.all() serializer_class = TaskSerializer @action( detail=False, methods=['post'], throttle_scope='ai_function' # AI content generation ) def auto_generate_content(self, request): # AI content generation logic pass class ImagesViewSet(BaseTenantViewSet): throttle_scope = 'writer' queryset = Image.objects.all() serializer_class = ImageSerializer @action( detail=False, methods=['post'], throttle_scope='image_gen' # Image generation ) def generate_images(self, request): # Image generation logic pass ``` **Auth Module:** **File:** `backend/auth/api/views.py` ```python from rest_framework.views import APIView from rest_framework.throttling import ScopedRateThrottle class LoginView(APIView): throttle_scope = 'auth' throttle_classes = [DebugScopedRateThrottle] def post(self, request): # Login logic pass class RegisterView(APIView): throttle_scope = 'auth' throttle_classes = [DebugScopedRateThrottle] def post(self, request): # Registration logic pass class ChangePasswordView(APIView): throttle_scope = 'auth' throttle_classes = [DebugScopedRateThrottle] def post(self, request): # Password change logic pass ``` **System Module:** **File:** `backend/system/api/viewsets.py` ```python class IntegrationSettingsViewSet(BaseTenantViewSet): throttle_scope = 'system' @action( detail=True, methods=['post'], throttle_scope='system_admin' # Admin operations ) def save(self, request, pk=None): # Save integration settings pass @action( detail=True, methods=['post'], throttle_scope='system_admin' ) def test(self, request, pk=None): # Test integration connection pass ``` **Billing Module:** **File:** `backend/billing/api/viewsets.py` ```python class CreditTransactionViewSet(BaseTenantViewSet): throttle_scope = 'billing' # Read-only operations use 'billing' scope # Write operations would use 'billing_admin' if implemented ``` **Estimated Time:** 6 hours --- ## Task 3: Override User/Account-Level Throttles (Optional) ### Goal Implement plan-based throttling that applies different rate limits based on user's subscription plan. ### Implementation Steps #### Step 3.1: Create Plan-Based Throttle Class **File:** `backend/igny8_core/api/throttles.py` **Implementation:** ```python """ Custom Throttle Classes for IGNY8 API """ from django.conf import settings from rest_framework.throttling import SimpleRateThrottle class PlanBasedRateThrottle(SimpleRateThrottle): """ Rate throttle that applies different limits based on user's plan. Plan tiers: - Free: 10/min for AI functions - Pro: 100/min for AI functions - Enterprise: Unlimited (or very high limit) Usage: throttle_scope = 'ai_function' # Base scope # Plan limits override base scope """ # Plan-based rate limits PLAN_RATES = { 'free': { 'ai_function': '10/min', 'image_gen': '15/min', 'content_write': '30/min', }, 'pro': { 'ai_function': '100/min', 'image_gen': '50/min', 'content_write': '100/min', }, 'enterprise': { 'ai_function': '1000/min', # Effectively unlimited 'image_gen': '200/min', 'content_write': '500/min', }, } def get_rate(self): """ Get rate limit based on user's plan. """ # Get scope from view scope = getattr(self.view, 'throttle_scope', None) if not scope: return None # Get user's plan user = self.request.user if not user or not user.is_authenticated: # Anonymous users use default rate return self.get_default_rate(scope) # Get account and plan account = getattr(user, 'account', None) if not account: return self.get_default_rate(scope) plan = getattr(account, 'plan', None) if not plan: return self.get_default_rate(scope) plan_name = plan.name.lower() if hasattr(plan, 'name') else 'free' # Get rate for plan and scope plan_rates = self.PLAN_RATES.get(plan_name, self.PLAN_RATES['free']) rate = plan_rates.get(scope, self.get_default_rate(scope)) return rate def get_default_rate(self, scope): """ Get default rate from settings if plan-based rate not found. """ throttle_rates = getattr(settings, 'REST_FRAMEWORK', {}).get('DEFAULT_THROTTLE_RATES', {}) return throttle_rates.get(scope, '100/min') def get_cache_key(self, request, view): """ Generate cache key based on user ID and scope. """ if request.user.is_authenticated: ident = request.user.pk else: ident = self.get_ident(request) scope = getattr(view, 'throttle_scope', None) return self.cache_format % { 'scope': scope, 'ident': ident } ``` #### Step 3.2: Apply Plan-Based Throttling **Update settings to use plan-based throttle for AI functions:** **File:** `backend/igny8_core/settings.py` ```python # For AI functions, use plan-based throttling # For other functions, use scoped throttling # This would require custom throttle class selection per ViewSet # Or use a mixin that selects throttle class based on scope ``` **Alternative: Use in specific ViewSets:** ```python from igny8_core.api.throttles import PlanBasedRateThrottle class TasksViewSet(BaseTenantViewSet): throttle_scope = 'ai_function' throttle_classes = [PlanBasedRateThrottle] # Use plan-based for this ViewSet @action(detail=False, methods=['post']) def auto_generate_content(self, request): # AI content generation pass ``` **Estimated Time:** 8 hours (optional, future phase) --- ## Task 4: Log + Expose Rate Limit Info ### Goal Ensure rate limit information is properly exposed to clients and logged for abuse detection. ### Implementation Steps #### Step 4.1: Verify Throttle Headers **DRF automatically adds these headers:** - `X-Throttle-Limit`: Maximum number of requests allowed - `X-Throttle-Remaining`: Number of requests remaining - `Retry-After`: Seconds to wait before retrying (when throttled) **Test in Postman:** 1. Make requests to throttled endpoint 2. Check response headers 3. Verify headers are present and correct **Example Response Headers:** ``` HTTP/1.1 200 OK X-Throttle-Limit: 10 X-Throttle-Remaining: 9 X-Throttle-Reset: 1640995200 ``` **When Throttled (429):** ``` HTTP/1.1 429 Too Many Requests X-Throttle-Limit: 10 X-Throttle-Remaining: 0 Retry-After: 60 ``` **Estimated Time:** 2 hours #### Step 4.2: Add Frontend Rate Limit Handling **File:** `frontend/src/services/api.ts` **Update fetchAPI to handle rate limits:** ```typescript export async function fetchAPI(endpoint: string, options?: RequestInit) { const response = await fetch(`${API_BASE_URL}${endpoint}`, { ...options, headers: { ...options?.headers, 'Authorization': `Bearer ${getToken()}`, }, }); // Check for rate limiting if (response.status === 429) { const retryAfter = response.headers.get('Retry-After'); const throttleLimit = response.headers.get('X-Throttle-Limit'); const throttleRemaining = response.headers.get('X-Throttle-Remaining'); // Show user-friendly error showNotification( `Rate limit exceeded. Please wait ${retryAfter} seconds before trying again.`, 'error' ); // Store rate limit info for UI if (throttleLimit && throttleRemaining !== null) { storeRateLimitInfo({ limit: parseInt(throttleLimit), remaining: parseInt(throttleRemaining), retryAfter: retryAfter ? parseInt(retryAfter) : 60, }); } throw new Error('Rate limit exceeded'); } // Check throttle headers on successful requests const throttleLimit = response.headers.get('X-Throttle-Limit'); const throttleRemaining = response.headers.get('X-Throttle-Remaining'); if (throttleLimit && throttleRemaining !== null) { const remaining = parseInt(throttleRemaining); const limit = parseInt(throttleLimit); const percentage = (remaining / limit) * 100; // Show warning if close to limit if (percentage < 20) { showNotification( `Warning: You have ${remaining} of ${limit} requests remaining.`, 'warning' ); } // Store for UI display storeRateLimitInfo({ limit, remaining, percentage, }); } return response; } ``` **Add Rate Limit Display Component:** **File:** `frontend/src/components/RateLimitIndicator.tsx` ```typescript import { useRateLimitStore } from '@/stores/rateLimitStore'; export function RateLimitIndicator() { const { limit, remaining, percentage } = useRateLimitStore(); if (!limit) return null; return (