Skip to main content

Overview

FootyCollect implements rate limiting to prevent abuse and ensure fair usage of API resources. Rate limiting is enforced at two levels:
  1. DRF Throttling - For internal API endpoints (/api/)
  2. django-ratelimit - For external API proxy endpoints (/fkapi/)
Rate limits apply per IP address for anonymous users and per user account for authenticated users.

DRF API Rate Limits (/api/)

Internal API endpoints use Django REST Framework’s throttling system with configurable rates.

Default Throttle Rates

User TypeDefault LimitConfigurable Via
Authenticated100 requests/hourDJANGO_DRF_USER_THROTTLE_RATE
Anonymous20 requests/hourDJANGO_DRF_ANON_THROTTLE_RATE

Throttle Classes

The API uses two throttle classes:
# From config/settings/base.py
REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.UserRateThrottle",
        "rest_framework.throttling.AnonRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "user": "100/hour",  # Authenticated users
        "anon": "20/hour",   # Anonymous users
    },
}
Authenticated users have 5x higher rate limits than anonymous users. Register and authenticate to increase your quota.

Customizing Throttle Rates

You can customize throttle rates using environment variables:
# .env file
DJANGO_DRF_USER_THROTTLE_RATE=200/hour
DJANGO_DRF_ANON_THROTTLE_RATE=50/hour

FKAPI Rate Limits (/fkapi/)

External API proxy endpoints use django-ratelimit for IP-based rate limiting.

Rate Limit Configuration

Endpoint PatternRate LimitScope
All /fkapi/ endpoints100 requests/hourPer IP address

Protected Endpoints

The following endpoints are rate-limited:
  • GET /fkapi/clubs/search/ - Search clubs
  • GET /fkapi/clubs/{club_id}/seasons/ - Get club seasons
  • GET /fkapi/clubs/{club_id}/seasons/{season_id}/kits/ - Get kits
  • GET /fkapi/kits/search/ - Search kits
  • GET /fkapi/kits/{kit_id}/ - Get kit details
  • GET /fkapi/brands/search/ - Search brands
  • GET /fkapi/competitions/search/ - Search competitions
  • GET /fkapi/seasons/search/ - Search seasons
  • GET /fkapi/filters/ - Get filter options

Implementation

Rate limiting is implemented using the @ratelimit decorator:
# From footycollect/api/views.py
from django_ratelimit.decorators import ratelimit

FKAPI_RATE_LIMIT = "100/h"

@ratelimit(key="ip", rate="100/h", method="GET")
@require_GET
def search_clubs(request):
    if getattr(request, "limited", False):
        return _rate_limited_response(request)
    # ... endpoint logic
FKAPI rate limits are IP-based and apply regardless of authentication status. All users from the same IP share the same quota.

Rate Limit Headers

When rate limits are exceeded, the API returns custom headers to help you track your usage.

FKAPI Response Headers

HeaderDescriptionExample
X-RateLimit-LimitMaximum requests allowed100/h
X-RateLimit-RemainingRequests remaining in window0
Retry-AfterSeconds until rate limit resets3600

Example Rate Limit Response

curl -X GET https://your-domain.com/fkapi/clubs/search/?keyword=barcelona \
  -H "Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b" \
  -i

HTTP Status Codes

429 Too Many Requests

When you exceed the rate limit, you’ll receive a 429 Too Many Requests response. DRF API Response:
{
  "detail": "Request was throttled. Expected available in 3600 seconds."
}
FKAPI Response:
{
  "error": "Rate limit exceeded"
}

Handling Rate Limits

Exponential Backoff

Implement exponential backoff to handle rate limit errors gracefully:
import time
import requests
from typing import Optional

def make_request_with_retry(
    url: str,
    headers: dict,
    max_retries: int = 3
) -> Optional[dict]:
    """Make API request with exponential backoff."""
    for attempt in range(max_retries):
        response = requests.get(url, headers=headers)
        
        if response.status_code == 200:
            return response.json()
        
        if response.status_code == 429:
            retry_after = int(response.headers.get('Retry-After', 60))
            wait_time = min(retry_after, 2 ** attempt * 60)  # Cap at retry_after
            print(f"Rate limited. Waiting {wait_time}s before retry...")
            time.sleep(wait_time)
            continue
        
        # Other errors
        response.raise_for_status()
    
    return None

# Usage
url = "https://your-domain.com/fkapi/clubs/search/?keyword=barcelona"
headers = {"Authorization": "Token your-token"}
data = make_request_with_retry(url, headers)

Rate Limit Monitoring

Track your rate limit usage to avoid hitting limits:
import requests
from datetime import datetime, timedelta

class RateLimitTracker:
    def __init__(self, limit: int, window_seconds: int):
        self.limit = limit
        self.window_seconds = window_seconds
        self.requests = []
    
    def can_make_request(self) -> bool:
        """Check if we can make a request without hitting rate limit."""
        now = datetime.now()
        window_start = now - timedelta(seconds=self.window_seconds)
        
        # Remove old requests outside the window
        self.requests = [req_time for req_time in self.requests if req_time > window_start]
        
        return len(self.requests) < self.limit
    
    def record_request(self):
        """Record a new request."""
        self.requests.append(datetime.now())
    
    def requests_remaining(self) -> int:
        """Get number of requests remaining in current window."""
        now = datetime.now()
        window_start = now - timedelta(seconds=self.window_seconds)
        self.requests = [req_time for req_time in self.requests if req_time > window_start]
        return self.limit - len(self.requests)

# Usage for FKAPI (100/hour)
tracker = RateLimitTracker(limit=100, window_seconds=3600)

if tracker.can_make_request():
    response = requests.get(url, headers=headers)
    tracker.record_request()
    print(f"Requests remaining: {tracker.requests_remaining()}")
else:
    print("Rate limit reached. Wait before making more requests.")

Best Practices

Anonymous users are limited to 20 requests/hour on DRF endpoints. Authenticate to get 100 requests/hour.
# Without authentication: 20 req/hour
curl -X GET https://your-domain.com/api/users/

# With authentication: 100 req/hour
curl -X GET https://your-domain.com/api/users/ \
  -H "Authorization: Token your-token"
Cache API responses to reduce the number of requests:
import requests
from functools import lru_cache
from datetime import datetime, timedelta

@lru_cache(maxsize=128)
def get_club_data(club_id: str, cache_time: str):
    """Cached club data (cache_time changes hourly)."""
    url = f"https://your-domain.com/fkapi/clubs/{club_id}/"
    return requests.get(url, headers=headers).json()

# Cache key changes every hour
cache_key = datetime.now().strftime("%Y-%m-%d-%H")
club = get_club_data("123", cache_key)
Instead of making multiple individual requests, batch operations where supported:
# Instead of multiple requests
for club_id in club_ids:
    get_club(club_id)  # 100 requests for 100 clubs

# Use search with broader queries when possible
results = search_clubs(keyword="premier league")  # 1 request
Always implement retry logic with exponential backoff:
try:
    response = requests.get(url, headers=headers)
    response.raise_for_status()
except requests.exceptions.HTTPError as e:
    if e.response.status_code == 429:
        retry_after = int(e.response.headers.get('Retry-After', 3600))
        print(f"Rate limited. Retry after {retry_after} seconds")
    else:
        raise
Track API usage to avoid unexpected rate limiting:
  • Log all API requests with timestamps
  • Monitor rate limit headers in responses
  • Set up alerts before hitting limits
  • Review usage patterns to optimize requests

Rate Limit Configuration

Environment Variables

Configure rate limits using environment variables in production:
# DRF API throttle rates
DJANGO_DRF_USER_THROTTLE_RATE=100/hour
DJANGO_DRF_ANON_THROTTLE_RATE=20/hour

Custom Throttle Rates

For custom deployments, you can modify throttle rates in config/settings/base.py:
REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_RATES": {
        "user": env.str("DJANGO_DRF_USER_THROTTLE_RATE", default="100/hour"),
        "anon": env.str("DJANGO_DRF_ANON_THROTTLE_RATE", default="20/hour"),
    },
}

Exception Handling

FootyCollect uses a custom DRF exception handler:
# From config/settings/base.py
REST_FRAMEWORK = {
    "EXCEPTION_HANDLER": "config.exceptions.drf_exception_handler",
}
This ensures consistent error responses across all API endpoints.

Requesting Higher Limits

If you need higher rate limits for production use:
  1. Contact support via GitHub Issues
  2. Describe your use case and required limits
  3. Provide details about your application
Higher rate limits are evaluated on a case-by-case basis for legitimate production use cases.

Summary

API TypeAuthenticatedAnonymousScopeReset Period
DRF API (/api/)100 req/hour20 req/hourPer user/IP1 hour
FKAPI (/fkapi/)100 req/hour100 req/hourPer IP1 hour

Next Steps