django-ratelimit
repository·main·Indexed 22 days ago
https://github.com/jsocol/django-ratelimitCache-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.
What's inside django-ratelimit
- 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.
Cache backend requirements for django-ratelimit
mainCritical Requirement: Atomic Increment
django_ratelimitrequires 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:- Persistent: Data survives restarts.
- Distributed: Works across multiple deployment worker instances (e.g., multiple UWSGI workers).
How rate limits are shared between views
mainIn 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
groupargument in the@ratelimitdecorator.# 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): passUnderstand risks of user-supplied data in ratelimit keys
mainUsing data directly from the client to generate rate limit keys introduces risks:
- 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. - Dangerous Headers: The
User-Agentheader 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.
- Trivial Circumvention: Clients can easily change values in GET parameters (
Define simple rate limits
mainSimple rates use the format
X/Yu, whereXis the number of requests,Yis the number of units, anduis the time unit. If the unituis omitted, it defaults to seconds (s).Supported units:
s: secondm: minuteh: hourd: 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 to100/5m).
5/s 100/5m 100/300Handle Client IP addresses behind reverse proxies
maindjango_ratelimitretrieves the client IP address fromrequest.META['REMOTE_ADDR'].If your application is behind a reverse proxy (like nginx or HAProxy),
REMOTE_ADDRwill 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 inREMOTE_ADDRto avoid ratelimiting the proxy itself.How rate limit windows work (Fixed vs Sliding)
mainSince 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 * Xrequests in a short period if the rate isX/u. Adjust your rate settings accordingly.Stacking multiple @ratelimit decorators
mainInstead of using a single decorator with multiple keys (which was deprecated in 0.5), you should now stack multiple
@ratelimitdecorators. 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): passMigrate from RatelimitMixin to @ratelimit decorator
mainIn version 3.0+,
RatelimitMixinwas removed. You should migrate to using the@ratelimitdecorator 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): passConfigure a compatible cache for django-ratelimit
maindjango_ratelimitrequires a cache backend that is shared across all worker threads, processes, and application servers, and supports atomic increment operations.Supported backends:
RedisMemcached
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.
Apply rate limits to Class-Based Views
mainTo use
@ratelimitwith Class-Based Views (CBVs), use Django's@method_decorator. You can decorate a specific method or the entire class.Note: Unless an explicit
groupis 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())), ]Send HTTP 429 Too Many Requests responses
mainBy default, Django Ratelimit triggers a 403 Forbidden response because its
Ratelimitedexception extends Django'sPermissionDenied. To comply with RFC 6585 and return anHTTP 429 Too Many Requestsstatus 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
requestandexceptionarguments and returns a response withstatus=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.RatelimitMiddlewareto yourMIDDLEWAREsetting (ideally toward the bottom of the list) and defineRATELIMIT_VIEWas 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'