Episode 12 helped you find performance bottlenecks with the slow query log and django-silk. But a fast API can still go down if it is hammered — a buggy client, a bot scanning endpoints, or brute-force attempts against the login API. This episode adds rate limiting to DRF using the built-in throttles and django-ratelimit, so each user can only call the API within the allowed limits.

1. Enable DRF’s Default Throttles

DRF ships with two throttle classes: AnonRateThrottle limits by IP for guests who are not logged in, and UserRateThrottle limits per account. Declare them in settings.py and every DRF view is protected at once. Use Redis as the cache backend (set up in episode 9) so the counters stay accurate when several Gunicorn workers run.

# settings.py
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": "100/day",
"user": "1000/hour",
},
}

With this configuration, guests get 100 requests per day and each logged-in account gets 1,000 requests per hour. Past the limit, DRF returns a 429 error with a Retry-After header saying how long to wait before calling again.

2. Per-Endpoint Limits with ScopedRateThrottle

Not every endpoint deserves the same limit: an endpoint that creates transactions needs a tighter cap than one that reads a list, because writes cost more resources and are easier to spam. ScopedRateThrottle sets a separate rate for each view.

# expenses/views.py
from rest_framework.throttling import ScopedRateThrottle
from rest_framework.viewsets import ModelViewSet
class ExpenseViewSet(ModelViewSet):
throttle_classes = [ScopedRateThrottle]
throttle_scope = "expense_create"
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
# settings.py — add to DEFAULT_THROTTLE_RATES
"expense_create": "30/min",

3. Stop Brute-Force with django-ratelimit

DRF throttling cannot stop brute-force against the login endpoint, because an attacker rotates IPs and each IP stays under the limit. django-ratelimit adds a per-IP blocking layer that temporarily locks the account after too many failed logins. Install it with pip install django-ratelimit and attach the decorator to the login view.

# accounts/views.py
from django_ratelimit.decorators import ratelimit
from django.http import JsonResponse
@ratelimit(key="ip", rate="5/m", block=True)
def login_view(request):
# handle login as usual
return JsonResponse({"detail": "Đăng nhập thành công"})

When blocked, the view returns a 429 — log it with the IP so Sentry (episode 7) can alert you. A limit of 5 attempts per minute is plenty for real users and tight enough for password guessers.

4. Verify and Monitor

Don’t trust the configuration until you have tried it yourself: use curl to call the transaction endpoint in a loop, count how many times it returns 429, or write a small test asserting a 429 after the limit is passed. In production, watch the 429 rate in your logs: if a real IP is blocked often, loosen the limit or throttle by account instead.

Apply It to Your Project

In the expense tracker app, apply all three layers of limits this week: the default throttle for the whole API, 30 requests per minute for the transaction-creation endpoint, and django-ratelimit at 5 attempts per minute for the login API. After a week, review the 429 logs and adjust: a correct limit is one real users never hit, while bots hit it immediately.

Related Posts

Frequently Asked Questions

Does rate limiting slow down the API for normal users?

No, if the limits are high enough. The counter is a single Redis cache read, costing less than a millisecond. Set the limit 3–5 times higher than the peak usage of a real user, then lower it gradually once you have data.

Should I limit by IP or by account?

By account once the user is logged in, because one IP can be an entire office sharing a NAT. Limit by IP only for endpoints that don’t require login, such as login or registration.

What should a client do when it gets a 429?

Read the Retry-After header, wait that many seconds, then retry with increasing backoff. Never retry immediately, because each retry also counts toward the limit and extends the block.

References