How To Implement API Rate Limiting in Your Django Project

How To Implement API Rate Limiting in Your Django Project

Have you ever seen a public API that only allows users to make a certain number of requests per minute or month? That API implemented something called rate-limiting to achieve such functionality.

Rate limiting is a mechanism that helps you control the number of requests a client can make to your service or resource. You typically use rate-limiting to protect your resource against abuse from both human users and spam bots. If they’re only allowed to make 5 requests per minute, you have a better chance at preventing spam.

Django REST Framework (DRF) provides some built-in mechanisms to help you implement rate-limiting. This article will show you how to leverage those mechanisms to implement rate-limiting for your API.

Prerequisites

This guide assumes the following about you:

  • You have at least intermediate-level knowledge working with Django and DRF.

  • You understand how class-based views work, and can use them.

  • You understand HTTP concepts such as requests, headers, etc.

If you don’t fulfill the above criteria, learning them before proceeding is a good idea.

Implementing API Rate Limiting in Django REST Framework

The approach to API rate-limiting in this guide is to have the developer or user sign up for an API key and then use it whenever they want to make requests. The key will be used to authenticate the requests and keep track of how many requests they make. The following steps will guide you on how to implement this approach.

Step 1: Create Your User Model, Serializer, and Authentication Views

Since API keys have to be assigned to users, it makes sense to have a user model and a way to authenticate users. You can choose to use Django’s built-in user model or create your own. I decided to create a custom user model, and you can find it in this GitHub gist:

If you decide to create a custom user model, ensure you update the appropriate setting in the settings.py file:

AUTH_USER_MODEL="yourApp.yourModel"

After creating your model, ensure your user serializer is in place to perform serialization for requests.

Finally, you should create views for signup and login functionalities. You can leverage the Simple JWT package to handle the login view. Here’s what your signup view might look like:

# users/views.py

from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework import generics
from rest_framework import status

from .serializers import UserSerializer

class Signup(generics.GenericAPIView):
    serializer_class = UserSerializer
    permission_classes = []

    def post(self, request:Request):
        serializer = self.serializer_class(data=request.data)
        if serializer.is_valid():
            serializer.save()
            response = {
                "message": "successful",
                "data": serializer.data
            }
            return Response(data=response, status=status.HTTP_201_CREATED)
        response = {
            "message": "failed",
            "info": serializer.errors
        }
        return Response(data=response, status=status.HTTP_400_BAD_REQUEST)

You can include other forms of authentication such as email authentication if you want to.

Your urls.py file should typically look like this if you’re using Simple JWT

# users/urls.py
from django.urls import path

# using simple jwt
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

from .views import Signup

urlpatterns = [
    path('signup/',Signup.as_view(), name="signup"),
    path('login/', TokenObtainPairView.as_view(), name="login"),
    path('login/refresh', TokenRefreshView.as_view(), name="login-refresh"),
]

Step 2: Create a Model for API Keys

There are multiple ways to generate API keys for users but for this guide, I have decided to automatically generate the API key immediately after a user signs up. I think it’s a pragmatic approach.

Before you create your model, you should consider how you want to generate your API keys. There are multiple ways to do this including using the Python secrets module to generate a random string, hashed values, base64, encoding, UUID fields, etc. Spend some time considering the pros and cons of these options before deciding. In this guide, I use the UUID field.

# users/models.py
from django.db import models
import uuid

class APIKey(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, editable=False)
    key = models.UUIDField(default=uuid.uuid4, editable=False)

The above model creates a one-to-one relationship with the User model and uses the UUID field to generate a key for the associated user. Note that the key is not editable.

Now that you have your model, you should consider a case where the key gets compromised. In such a situation, you want the developer/user to be able to regenerate their API key. You can achieve this by creating a custom method within the APIKey model:

class APIKey(models.Model):
    # your fields and other methods go here

    def regenerate_key(self):
        self.key = uuid.uuid4()
        self.save()

The limitation of this model is that it doesn’t generate a key for the user when a user instance is created. You can do this by using the post_save signal in Django. This will fire immediately after a user completes the signup process.

# users/models.py
from django.db.models.signals import post_save
from django.dispatch import receiver

# signal to create an API key whenever a new user is created
@receiver(post_save, sender=User)
def create_api_key(sender, instance, created, **kwargs):
    if created:
        APIKey.objects.create(user=instance)

If you don’t fully understand how the [models.py](http://models.py) file is structured, you can reference it in the GitHub repository.

After you create your model, you should create an associating serializer for it:

# users/serializers.py
from rest_framework import serializers
from .models import APIKey

class APIKeySerializer(serializers.ModelSerializer):
    class Meta:
        model = APIKey
        fields = ['key']

Step 3: Create a View for API Keys

To help users access their API keys and regenerate them when necessary, you need a view mapped to a URL. Here’s what your view should typically look like:

# users/views.py

class GetAPIKey(generics.GenericAPIView):
    serializer_class = APIKeySerializer

    def get(self, request:Request):
        api_key = APIKey.objects.get(user=request.user)
        serializer = self.serializer_class(instance=api_key)
        response = {
            "message":"Successful",
            "data":serializer.data
        }
        return Response(data=response, status=status.HTTP_200_OK)

    def put(self, request:Request):
        api_key = APIKey.objects.get(user=request.user)
        api_key.regenerate_key()
        serializer = self.serializer_class(instance=api_key)
        response = {
            "message":"Successful",
            "data":serializer.data
        }
        return Response(data=response, status=status.HTTP_200_OK)

After this, you should create a corresponding URL:

# users/urls.py
from .views import GetAPIKey

urlpatterns = [
    # add other URLs here
    path('api_key/get/', GetAPIKey.as_view()),
]

NOTE: Ensure you set the appropriate permission and authentication classes for your views as you see fit. You can also set default values for them in your settings.py file.

Step 4: Create a Throttle To Keep Track of Requests Made by an API Key

Throttling is Django’s approach to rate-limiting. To achieve the desired behavior explained above, you need to create a throttle for the API key. The official DRF documentation gives a comprehensive explanation of throttles and throttling.

According to the documentation, DRF provides 3 types of throttles to use. This guide will leverage one of them. It’s called the UserRateThrottle.

Create a throttling.py file to handle your throttles. The exact location of this file depends on your preference and use case. For this guide, I created mine within my project directory. Feel free to create yours within your app directory if that works for you.

# throttling.py
from rest_framework.throttling import UserRateThrottle

class APIKeyRateThrottle(UserRateThrottle):
    scope = 'api_key'

    def get_cache_key(self, request, view):
        api_key = request.headers.get("Authorization")
        if api_key:
            return f'{self.scope}_{api_key}'
        return None

The code above creates a throttle class that inherits from the UserRateThrottle class available in DRF. Within the new throttle class, it sets the scope to ‘api_key’. What this means in simple terms is that the throttle should be applied only to the context of API keys.

Finally, it overrides the get_cache_key method available in UserRateThrottle. This method generates a cache key that will keep track of requests made with each API key.

Next, you should add these settings to your settings file:

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'yourDirectory.throttling.APIKeyRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'api_key': '5/minute',
    },
}

In the above setting, I set the APIKeyRateThrottle as the default throttle class for the entire project. If this defeats your use case, you can set individual throttle classes on the specific views.

I also set the default throttle rate to be 5 requests per minute for the api_key scope. Modify this to fit your use case.

Because of the above setting, you need to exclude your signup and API key views from inheriting the throttle class. You can achieve this by going to the respective views and including this line of code under the definition of the serializer_class:

throttle_classes = []

Step 5: Create an App To Apply Rate Limiting

At this stage, you need an app to apply your rate-limiting techniques. If you don’t already have one, ensure you create one. I will create a simple app called books

python manage.py startapp books

Next, create a model inside it:

# books/models.py
from django.db import models
from django.contrib.auth import get_user_model

User = get_user_model()

class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    publication_date = models.DateField(auto_now=True, auto_now_add=False)

    def __str__(self):
        return self.title

Also, create a serializer:

from rest_framework import serializers
from .models import Book

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = "__all__"
        read_only_fields = ["id", "author", "publication_date"]

    def create(self, validated_data):
        author = self.context['api_key'].user # context from the view to associate the user with a book instance
        validated_data['author']=author
        return super().create(validated_data)

The next thing is your views and this is where things get interesting.

Step 6: Create Views for Your New App

The first goal is to ensure that only requests with valid API keys can access the endpoints. An intuitive approach to this is to intercept the request before it gets sent to the appropriate HTTP method handler (get, post, patch, etc methods) and check the validity of the API key provided. If it’s valid, the request will be sent to the valid handler for processing. But how do you achieve this?

In DRF class-based views, there’s a method called dispatch. The dispatch method is responsible for delegating requests to the appropriate handlers. This means that all requests will go through the dispatch method before getting to their appropriate handlers. It makes sense to intercept the request and check for the API key validity within the dispatch method.

With this approach, you might have to override the dispatch method of all your class-based views which means you’ll repeat yourself multiple times. A more pragmatic approach to this is to define a custom view, override the dispatch method, and inherit the custom view in other views. Here’s an example:

# books/views.py
class CustomAPIView(generics.GenericAPIView):
    permission_classes = []
    authentication_classes = []

    def dispatch(self, request, *args, **kwargs):
        api_key = request.headers.get("Authorization")
        if api_key:
            try:
                api_key_obj = APIKey.objects.get(key=api_key)
                return super().dispatch(request, *args, **kwargs)
            except Exception as e:
                logger.warning(str(e)) # log the exception
                return JsonResponse({"message": "Unauthorized. Invalid API key"}, status=status.HTTP_401_UNAUTHORIZED)
        else:
            return JsonResponse({"message": "API key required"}, status=status.HTTP_400_BAD_REQUEST)

In the view above, the dispatch method checks the authorization header (since that’s where we want users to put their API keys) and retrieves the API key in it. Then it performs a query to check if the key found in the authorization header exists in the database. If the key exists, the appropriate method handler is called, otherwise, a 401 error is sent to the client and logs the error.

Now that you have this view, you can inherit it in other views that require rate-limiting. So a basic view to list and create books will look like this:

# books/views.py
class BookListCreate(CustomAPIView):
    serializer_class = BookSerializer

    def get(self, request:Request):

        api_key = request.headers.get("Authorization")
        api_key_obj = APIKey.objects.get(key=api_key)

        queryset = Book.objects.filter(author=api_key_obj.user)
        serializer = self.serializer_class(instance=queryset, many=True)
        response = {
             "message":"successful",
             "data": serializer.data
         }
        return Response(data=response, status=status.HTTP_200_OK)

    def post(self, request:Request):
        api_key = request.headers.get("Authorization")
        api_key_obj = APIKey.objects.get(key=api_key)
        serializer = self.serializer_class(data=request.data, context={'api_key':api_key_obj})
        if serializer.is_valid():
            serializer.save()
            response = {
             "message":"successful",
             "data": serializer.data
             }
            return Response(data=response, status=status.HTTP_201_CREATED)

        response = {
             "message":"Failed",
             "info": serializer.errors
             }
        return Response(data=response, status=status.HTTP_400_BAD_REQUEST)

In the view above, the get() method returns only the books associated with the owner of the API key. The post() method also sends the api_key_obj to the serializer in order to associate a book instance with the API key.

With these implementations in place, your app will have the following behavior:

  • users can’t access your endpoints without a valid API key

  • only the books created by the user will be sent to them when they make a GET request

  • each API key gets a maximum of 5 requests per minute

Conclusion

There are multiple ways to implement API rate-limiting for your project but the underlying concepts are similar. For instance, you can decide to have the API key sent as a path or query parameter instead of a header depending on your use case.

There are other aspects to consider when implementing API rate-limiting such as granularity, and things like rate limit storage and database load

You should carefully consider what you want to achieve when making such decisions.