In Django REST Framework, how would you give a public search endpoint its own tighter limit using ScopedRateThrottle and throttle_scope?
answer
- a named bucket per view
- an attribute on the view
- the rate lives in the same settings dict
- all listed throttles are consulted
basics
~20 sAdd ScopedRateThrottle to the throttle classes, set throttle_scope = 'search' on the search view, and define a 'search' rate in DEFAULT_THROTTLE_RATES. Only views with a throttle_scope are limited, each scope counted separately per user or IP.
solid answer
~40 s`ScopedRateThrottle` reads the view's `throttle_scope` at request time and looks its rate up in `DEFAULT_THROTTLE_RATES`, so `throttle_scope = "search"` plus `"search": "20/min"` gives search its own counter, keyed by user primary key or, for anonymous callers, by IP. Views without `throttle_scope` are not touched by it, so it can sit in `DEFAULT_THROTTLE_CLASSES` next to the anon and user classes. DRF asks **every** listed throttle, so a search request also counts against the general `anon` or `user` allowance, and the reported wait is the longest among the refusing classes. A scope without a configured rate raises `ImproperlyConfigured` at request time. Function views get the scope from the `@throttle_scope` decorator, added in DRF 3.18.0. For burst-plus-sustained limits, subclass `UserRateThrottle` twice with different `scope` names.
code
python · 13 linesfrom rest_framework import generics
from .models import Article
from .serializers import ArticleSerializer
class ArticleSearchView(generics.ListAPIView):
serializer_class = ArticleSerializer
throttle_scope = "search"
def get_queryset(self):
term = self.request.query_params.get("q", "")
return Article.objects.filter(title__icontains=term)go deeper
Know that throttle_scope on a view plus a matching rate in DEFAULT_THROTTLE_RATES gives that view its own limit.
Explain how ScopedRateThrottle picks its rate at request time, how keys include the scope, and why all listed throttles are consulted.
Design burst and sustained scopes for expensive endpoints, and watch for view-level throttle_classes that silently drop the global limits.
Decide which endpoints deserve their own allowance and how rates are reviewed as traffic and query costs change.
## The problem: one endpoint is more expensive than the rest A public search endpoint — `GET /api/search/?q=...` — typically runs a heavier query than a detail lookup, and anonymous scrapers find it first. The global `anon` and `user` throttles in Django REST Framework (DRF) apply one allowance to the whole API, so they are either too loose for search or too tight for everything else. DRF's answer is a **scope**: a named allowance attached to particular views. ## How `ScopedRateThrottle` works `ScopedRateThrottle` postpones choosing its rate until it sees the view: 1. In `allow_request()` it reads `getattr(view, "throttle_scope", None)`. 2. If the view has no scope, it returns `True` — the class does nothing for that view. 3. Otherwise it fetches `DEFAULT_THROTTLE_RATES[scope]`; a missing key raises `ImproperlyConfigured` ("No default throttle rate set for 'search' scope"). 4. It builds the cache key `throttle_<scope>_<ident>`, where `ident` is `request.user.pk` for authenticated users and the client IP otherwise, and checks the stored history. Because scopes are part of the key, `search` and `uploads` counters for the same user never mix. ```python # settings.py REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES": [ "rest_framework.throttling.AnonRateThrottle", "rest_framework.throttling.UserRateThrottle", "rest_framework.throttling.ScopedRateThrottle", ], "DEFAULT_THROTTLE_RATES": { "anon": "1000/day", "user": "10000/day", "search": "20/min", }, } ``` ## Attaching the scope | View style | How to set the scope | |---|---| | `APIView` or generic view | class attribute `throttle_scope = "search"` | | `@api_view` function | `@throttle_scope("search")` under `@api_view` (DRF 3.18.0+) | | one action of a viewset | give search its own view, or override `get_throttles()` for that action | A dedicated `ListAPIView` for search is the simplest route: its scope, pagination and filtering are all visible in one class. ## Several throttles at once `APIView.check_throttles()` loops over **all** throttle instances; it does not stop at the first refusal. Consequences worth knowing: - A search request by an anonymous client is counted by `anon` **and** `search`; either can refuse it. - A throttle that allows the request records it even if another throttle refuses the same request, so refused requests can still consume the looser allowance. - When several refuse, the `Retry-After` value is the **largest** of their waits (ignoring any that cannot compute one). - `AnonRateThrottle` skips authenticated users, but `ScopedRateThrottle` does not: logged-in users are counted under `search` by user primary key. ## Burst and sustained limits Two scopes on one class family express "short bursts, but not all day": ```python from rest_framework.throttling import UserRateThrottle class SearchBurstThrottle(UserRateThrottle): scope = "search_burst" class SearchSustainedThrottle(UserRateThrottle): scope = "search_sustained" ``` List both on the search view and define `"search_burst": "10/s"` and `"search_sustained": "500/hour"`. Each subclass keeps its own key because the scope is part of it. ## Checklist for a scoped endpoint - Add `ScopedRateThrottle` once, globally; it is inert for unscoped views. - Define every scope's rate, or requests to that view fail with a configuration error. - Remember a view-level `throttle_classes` replaces the global list, so it must include the scoped class too. - Test that the 21st request in a minute answers 429 with `Retry-After`.
- Why is a refused search request in DRF sometimes still counted against the user's daily allowance?`check_throttles()` asks every throttle before deciding. If the daily `user` throttle allows the request, it records the timestamp in its history, even though the `search` throttle then refuses it. A client hammering search can therefore burn part of its general allowance with requests that all returned 429.
- In DRF, a search view sets throttle_classes = [ScopedRateThrottle] and throttle_scope = 'search'. What changed?The view's list replaces `DEFAULT_THROTTLE_CLASSES`, so the global `anon` and `user` throttles no longer run on it. Only the `search` scope applies. That may be intended, but it is a common accident when someone meant to add a limit rather than swap the whole list.
saying these in an interview costs you the question
- ScopedRateThrottle limits every view once it is in DEFAULT_THROTTLE_CLASSES.
- DRF stops checking throttles after the first one refuses.
- A missing scope rate just leaves that view unthrottled.
- ScopedRateThrottle ignores logged-in users, like AnonRateThrottle.
- All scopes for one user share a single counter.