10 KiB
Rate Limiting Communication Guide
INT-013: Add API rate limiting communication
Date: 2025-12-25
Status: Completed
Overview
This guide documents how rate limiting is communicated between the Veza backend API and frontend clients. It covers response formats, headers, error handling, and best practices.
Backend Rate Limiting
Rate Limit Configuration
The Veza API implements multiple rate limiting strategies:
-
IP-based rate limiting (unauthenticated users)
- Default: 100 requests per minute
- Burst: 10 requests
-
User-based rate limiting (authenticated users)
- Default: 1000 requests per minute
- Burst: 100 requests
-
Endpoint-specific rate limiting
- Login attempts: Configurable (default: 5 attempts per 15 minutes)
- Upload endpoints: 10 uploads per hour per user
Rate Limit Response Format
When a rate limit is exceeded, the backend returns:
HTTP Status: 429 Too Many Requests
Headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1703509200
Retry-After: 60
Response Body (Standardized APIResponse format):
{
"success": false,
"error": {
"code": 429,
"message": "Rate limit exceeded. Please try again later.",
"details": [
{
"field": "rate_limit",
"message": "You have exceeded the rate limit of 100 requests per minute"
}
],
"retry_after": 60,
"limit": 100,
"remaining": 0,
"reset": 1703509200
}
}
Response Fields
code: Error code (429 for rate limiting)message: Human-readable error messagedetails: Array of validation/error detailsretry_after: Number of seconds to wait before retryinglimit: Maximum number of requests allowedremaining: Number of requests remaining (0 when limit exceeded)reset: Unix timestamp when the rate limit resets
Headers Explained
X-RateLimit-Limit: Maximum number of requests allowed in the time windowX-RateLimit-Remaining: Number of requests remaining in the current windowX-RateLimit-Reset: Unix timestamp when the rate limit window resetsRetry-After: Number of seconds to wait before retrying (RFC 7231)
Frontend Rate Limit Handling
Error Detection
The frontend detects rate limit errors by:
- HTTP Status Code:
429 - Response Format: Standardized
APIResponsewithsuccess: false - Error Code:
429in error object
Error Parsing
The frontend parses rate limit errors in apiErrorHandler.ts:
if (status === 429) {
// Extract rate limit information from headers
const rateLimitLimit = headers['x-ratelimit-limit'];
const rateLimitRemaining = headers['x-ratelimit-remaining'];
const rateLimitReset = headers['x-ratelimit-reset'];
const retryAfter = headers['retry-after'] || data?.error?.retry_after || 60;
// Calculate time until reset
const resetTime = rateLimitReset
? new Date(rateLimitReset * 1000)
: undefined;
const secondsUntilReset = resetTime
? Math.max(0, Math.ceil((resetTime.getTime() - Date.now()) / 1000))
: retryAfter;
return {
code: 429,
message: data?.error?.message || 'Trop de requêtes. Veuillez patienter avant de réessayer.',
retry_after: secondsUntilReset,
rate_limit: {
limit: rateLimitLimit,
remaining: rateLimitRemaining,
reset: resetTime?.toISOString(),
},
};
}
User Notification
When a rate limit error occurs:
-
Toast Notification: Shows error message with retry information
toast.error(errorMessage, { duration: 8000, // Longer duration for rate limit errors }); -
Error Message: User-friendly message in French
"Trop de requêtes. Veuillez patienter quelques instants" -
Retry Information: Includes time until reset
"Réessayez dans 60 secondes"
Automatic Retry
The frontend implements automatic retry for rate limit errors:
-
Retry Configuration: Rate limit errors are included in retryable status codes
retryableStatusCodes: [429, 500, 502, 503, 504] -
Exponential Backoff: Retries use exponential backoff
// For 429 errors, use retry_after if available if (status === 429 && retryAfter) { delay = retryAfter * 1000; // Use retry_after in seconds } -
Retry Limits: Maximum retries configured to prevent infinite loops
maxRetries: 3
Best Practices
For Backend Developers
-
Always Include Headers: Include all rate limit headers in responses
c.Header("X-RateLimit-Limit", strconv.Itoa(limit)) c.Header("X-RateLimit-Remaining", strconv.Itoa(remaining)) c.Header("X-RateLimit-Reset", strconv.FormatInt(resetTime, 10)) c.Header("Retry-After", strconv.Itoa(retryAfter)) -
Use Standardized Format: Use
APIResponseformat for consistencyc.JSON(http.StatusTooManyRequests, gin.H{ "success": false, "error": gin.H{ "code": 429, "message": "Rate limit exceeded. Please try again later.", "retry_after": retryAfter, "limit": limit, "remaining": 0, "reset": resetTime, }, }) -
Calculate Accurate Reset Time: Use actual window reset time, not fixed values
resetTime := time.Now().Add(window).Unix() -
Provide Clear Messages: Include helpful error messages
"message": fmt.Sprintf("You have exceeded the rate limit of %d requests per %v", limit, window)
For Frontend Developers
-
Check Headers First: Always check rate limit headers before parsing body
const rateLimitLimit = headers['x-ratelimit-limit']; const rateLimitRemaining = headers['x-ratelimit-remaining']; -
Use Retry-After: Respect the
Retry-Afterheader for retry timingconst retryAfter = headers['retry-after'] || 60; -
Show User-Friendly Messages: Display clear messages to users
"Trop de requêtes. Veuillez patienter quelques instants" -
Implement Exponential Backoff: Use exponential backoff for retries
delay = Math.min(delay * 2, maxDelay); -
Track Rate Limit State: Store rate limit information for UI display
rate_limit: { limit: rateLimitLimit, remaining: rateLimitRemaining, reset: resetTime, }
Rate Limit Headers Usage
Reading Headers in Frontend
// Get rate limit information from response headers
const getRateLimitInfo = (response: AxiosResponse) => {
const headers = response.headers;
return {
limit: parseInt(headers['x-ratelimit-limit'] || '0', 10),
remaining: parseInt(headers['x-ratelimit-remaining'] || '0', 10),
reset: parseInt(headers['x-ratelimit-reset'] || '0', 10),
retryAfter: parseInt(headers['retry-after'] || '0', 10),
};
};
Displaying Rate Limit Status
// Show rate limit status in UI
const RateLimitIndicator = ({ rateLimit }) => {
if (!rateLimit) return null;
const resetTime = new Date(rateLimit.reset * 1000);
const timeUntilReset = Math.ceil((resetTime.getTime() - Date.now()) / 1000);
return (
<div className="rate-limit-indicator">
<span>Requests: {rateLimit.remaining} / {rateLimit.limit}</span>
{rateLimit.remaining === 0 && (
<span>Resets in {timeUntilReset} seconds</span>
)}
</div>
);
};
Testing Rate Limiting
Backend Tests
func TestRateLimitMiddleware(t *testing.T) {
router := setupTestRouter()
// Make requests up to limit
for i := 0; i < 100; i++ {
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/v1/tracks", nil)
router.ServeHTTP(w, req)
if i < 100 {
assert.Equal(t, http.StatusOK, w.Code)
} else {
assert.Equal(t, http.StatusTooManyRequests, w.Code)
assert.Equal(t, "0", w.Header().Get("X-RateLimit-Remaining"))
}
}
}
Frontend Tests
describe('Rate Limit Handling', () => {
it('should parse rate limit error correctly', () => {
const error = {
response: {
status: 429,
headers: {
'x-ratelimit-limit': '100',
'x-ratelimit-remaining': '0',
'x-ratelimit-reset': '1703509200',
'retry-after': '60',
},
data: {
success: false,
error: {
code: 429,
message: 'Rate limit exceeded',
retry_after: 60,
},
},
},
};
const apiError = parseApiError(error);
expect(apiError.code).toBe(429);
expect(apiError.retry_after).toBe(60);
expect(apiError.rate_limit.limit).toBe(100);
});
});
Common Scenarios
Scenario 1: User Exceeds Rate Limit
- User makes 101 requests in 1 minute (limit: 100)
- Backend returns
429with rate limit headers - Frontend shows toast: "Trop de requêtes. Veuillez patienter quelques instants"
- Frontend waits
retry_afterseconds before retrying - Request automatically retries after delay
Scenario 2: Rate Limit Reset
- User hits rate limit at 10:00:00
- Backend sets
X-RateLimit-Reset: 1703509260(10:01:00) - Frontend calculates: 60 seconds until reset
- User sees: "Réessayez dans 60 secondes"
- At 10:01:00, rate limit resets automatically
Scenario 3: Different Limits for Authenticated Users
- Unauthenticated user: 100 requests/minute
- Authenticated user: 1000 requests/minute
- Headers reflect appropriate limit
- Frontend displays limit based on user status
Troubleshooting
Issue: Rate limit not working
Check:
- Rate limit middleware is applied
- Redis is running (if using Redis-based rate limiting)
- Headers are being set correctly
Issue: Frontend not showing rate limit errors
Check:
- Error handler is checking for status 429
- Headers are being parsed correctly
- Toast notifications are enabled
Issue: Retry not respecting retry_after
Check:
retry_afteris being extracted from headers- Retry delay is using
retry_aftervalue - Exponential backoff is not overriding
retry_after
References
- RFC 7231 - Retry-After Header
- HTTP 429 Status Code
ERROR_RESPONSE_STANDARD.md- Standard error response formatREQUEST_RESPONSE_VALIDATION_GUIDE.md- Validation guide
Last Updated: 2025-12-25
Maintained By: Veza Backend Team