django-ratelimit

repository·main·Indexed 22 days ago

https://github.com/jsocol/django-ratelimit

Cache-based rate-limiting for Django version 4.1.0. It provides a decorator-based mechanism to limit Django views based on IP addresses or request parameters (GET/POST), utilizing the Django cache framework to track request counts. Features include the @ratelimit decorator, RatelimitMiddleware for custom exception handling, and utility functions like is_ratelimited() and get_usage() for detailed rate limit status.

Tokens
13.1K
Snippets
44
Records
58
Agent score
77%

What's inside django-ratelimit

  1. Overview of Django Ratelimit

    main
    Django Ratelimit is a library that provides a decorator to rate-limit Django views. It allows you to restrict the frequency of requests based on the client's IP address or specific fields within the request, such as GET or POST variables.
  2. Cache backend requirements for django-ratelimit

    main

    Critical Requirement: Atomic Increment

    django_ratelimit requires a Django cache backend that supports atomic increment operations.

    • Supported: Memcached and Redis backends.
    • Not Supported: The Django database backend.

    Deployment Considerations

    Before activating django-ratelimit, ensure your cache backend is:

    1. Persistent: Data survives restarts.
    2. Distributed: Works across multiple deployment worker instances (e.g., multiple UWSGI workers).
  3. How rate limits are shared between views

    main

    In modern versions (0.5+), rate limits are not shared between different views/methods by default, even if they use the same keys and rates. This prevents one view from accidentally exhausting the quota for another.

    To explicitly force multiple views to share the same rate limit, use the group argument in the @ratelimit decorator.

    # These two views share a limit because they belong to the same group
    @ratelimit(group='lists', key='user', rate='100/h')
    def user_list(request):
        pass
    
    @ratelimit(group='lists', key='user', rate='100/h')
    def group_list(request):
        pass
    
    # This view is limited separately, even though it uses the same key and rate
    @ratelimit(key='user', rate='100/h')
    def another_view(request):
        pass
  4. Understand risks of user-supplied data in ratelimit keys

    main

    Using data directly from the client to generate rate limit keys introduces risks:

    1. Trivial Circumvention: Clients can easily change values in GET parameters (key='get:X'), POST data (key='post:X'), or headers (key='header:x-x') on every request to bypass limits.
    2. Dangerous Headers: The User-Agent header is particularly dangerous because attackers can rotate it constantly, and many legitimate users may share the same value.

    Best Practice: Only use headers that are guaranteed to be set by your trusted web server or reverse proxy and cannot be manipulated by the client.

  5. Define simple rate limits

    main

    Simple rates use the format X/Yu, where X is the number of requests, Y is the number of units, and u is the time unit. If the unit u is omitted, it defaults to seconds (s).

    Supported units:

    • s: second
    • m: minute
    • h: hour
    • d: day

    Special values:

    • 0/u: Disallows all requests (e.g., 0/s).
    • None: Indicates no limit; usage will not be tracked.

    Examples:

    • 5/s: Five requests per second.
    • 100/5m: One hundred requests per five minutes.
    • 100/300: One hundred requests per 300 seconds (equivalent to 100/5m).
    5/s
    100/5m
    100/300
  6. Handle Client IP addresses behind reverse proxies

    main

    django_ratelimit retrieves the client IP address from request.META['REMOTE_ADDR'].

    If your application is behind a reverse proxy (like nginx or HAProxy), REMOTE_ADDR will contain the IP address of the proxy rather than the actual client. You must configure your web server or application to ensure the correct client IP is available in REMOTE_ADDR to avoid ratelimiting the proxy itself.

  7. How rate limit windows work (Fixed vs Sliding)

    main

    Since version 0.5, Django Ratelimit uses fixed windows instead of sliding windows.

    • Sliding Windows (Old): The window moves with every request, which could unfairly catch good actors in a continuous loop of rate limiting.
    • Fixed Windows (New): Limits are calculated against fixed time periods. These windows are automatically staggered based on the key value to prevent all limits from expiring at the exact same time (e.g., at the top of the hour).

    Warning: Because windows are fixed, you may occasionally see up to 2 * X requests in a short period if the rate is X/u. Adjust your rate settings accordingly.

  8. Stacking multiple @ratelimit decorators

    main

    Instead of using a single decorator with multiple keys (which was deprecated in 0.5), you should now stack multiple @ratelimit decorators. This allows for complex patterns like burst limits (e.g., a high rate per minute combined with a lower rate per hour) or different limits for different HTTP methods.

    # Example: Hourly rate limit with a per-minute burst limit
    @ratelimit(key='ip', rate='100/m')
    @ratelimit(key='ip', rate='1000/h')
    def myview(request):
        pass
    
    # Example: Different limits for GET and POST
    @ratelimit(key='ip', method='GET', rate='1000/h')
    @ratelimit(key='ip', method='POST', rate='100/h')
    def maybe_expensive(request):
        pass
  9. Migrate from RatelimitMixin to @ratelimit decorator

    main

    In version 3.0+, RatelimitMixin was removed. You should migrate to using the @ratelimit decorator combined with @method_decorator. This allows you to apply multiple limits to the same method, which was not possible with the mixin.

    # Old way (RatelimitMixin)
    class MyView(RatelimitMixin, View):
        ratelimit_key = 'ip'
        ratelimit_rate = '10/m'
        ratelimit_method = 'GET'
    
        def get(self, request):
            pass
    
    # New way (@method_decorator)
    class MyView(View):
        @method_decorator(ratelimit(key='ip', rate='10/m', method='GET'))
        def get(self, request):
            pass
  10. Configure a compatible cache for django-ratelimit

    main

    django_ratelimit requires a cache backend that is shared across all worker threads, processes, and application servers, and supports atomic increment operations.

    Supported backends:

    • Redis
    • Memcached

    Unsupported backends:

    • local memory (not shared across processes/servers)
    • filesystem (not shared across processes/servers)
    • database (does not support atomic increments)

    Warning: Using a backend without atomic increment operations can cause race conditions, leading to undercounting usage and allowing more traffic than intended.

  11. Apply rate limits to Class-Based Views

    main

    To use @ratelimit with Class-Based Views (CBVs), use Django's @method_decorator. You can decorate a specific method or the entire class.

    Note: Unless an explicit group is provided, different methods of a class-based view will be limited separately.

    from django.utils.decorators import method_decorator
    from django.views.generic import View
    from django_ratelimit.decorators import ratelimit
    
    # Decorating a specific method
    class MyView(View):
        @method_decorator(ratelimit(key='ip', rate='1/m', method='GET'))
        def get(self, request):
            pass
    
    # Decorating the whole class (applying to a specific method name)
    @method_decorator(ratelimit(key='ip', rate='1/m', method='GET'), name='get')
    class MyOtherView(View):
        def get(self, request):
            pass
    
    # Wrapping the view in urlpatterns
    urlpatterns = [
        path('/', ratelimit(key='ip', method='GET', rate='1/m')(MyView.as_view())),
    ]
  12. Send HTTP 429 Too Many Requests responses

    main

    By default, Django Ratelimit triggers a 403 Forbidden response because its Ratelimited exception extends Django's PermissionDenied. To comply with RFC 6585 and return an HTTP 429 Too Many Requests status code, you must configure a custom error view and register it via middleware.

    Step 1: Create a custom error view

    Define a view that accepts request and exception arguments and returns a response with status=429. You can return HTML, JSON, or any other format required by your application.

    Step 2: Configure Middleware and RATELIMIT_VIEW

    Add django_ratelimit.middleware.RatelimitMiddleware to your MIDDLEWARE setting (ideally toward the bottom of the list) and define RATELIMIT_VIEW as a dotted-path to your custom view.

    # myapp/views.py
    def ratelimited_error(request, exception):
        # e.g. to return HTML
        return render(request, 'ratelimited.html', status=429)
    
    # In settings.py
    MIDDLEWARE = (
        # ...
        'django_ratelimit.middleware.RatelimitMiddleware',
        # ...
    )
    
    RATELIMIT_VIEW = 'myapp.views.ratelimited_error'