Production Tips for Django Apps

July 28, 2022

A few things I make sure to keep in mind when building an application for real world users with Django:

1. Custom User model

Even if the default User model is enough for your project. It’s recommended to use a custom User model when you’re starting a new project.

This will make it much easier to deal with customization when the need arises.

To do this, we can inherit from Django’s default User model and add any fields our app needs. The following snippet illustrates this:

from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import ugettext_lazy as _

from .managers import CustomUserManager


class CustomUser(AbstractUser):
    username = None
    email = models.EmailField(_('email address'), unique=True)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    objects = CustomUserManager()

    def __str__(self):
        return self.email

For more information on this topic, you may refer to this article which covers it thoroughly.

2. Configure Gunicorn

Django’s primary deployment platform is WSGI, the Python standard for web servers.

Gunicorn is by far the most popular WSGI server used with Django. Especially while deploying on PythonAnywhere or Heroku.

Like most beginners, I was unaware about how to configure Gunicorn when I was starting out. And I didn’t know how this would affect my app’s performance.

Requests were taking almost a minute (Heroku’s timeout) when a few people were using my app.

I knew I was missing something simple and that upgrading my server wasn’t the answer. On doing some research, I learnt something about how Gunicorn handles concurrent requests.

By default, Gunicorn uses 1 worker process for handling requests. This number determines the greatest number of concurrent requests our app can handle.

For best performance, we must set the number of workers to (2*CPU)+1. So for a dual core machine the suggested number of workers is 5.

To do this, we must create a file gunicorn.conf.py and declare a variable workers = 5. The following snippet showcases my application’s Gunicorn file for reference:

workers = 5
errorlog = '-'
loglevel = 'debug'
accesslog = '-'
access_log_format = "{'remote_ip':'%({X-Real-IP}i)s','request_id':'%({X-Request-Id}i)s','response_code':'%(s)s','request_method':'%(m)s','request_path':'%(U)s','request_querystring':'%(q)s','request_timetaken':'%(D)s','response_length':'%(B)s'}"
timeout = 70

Here’s the article that helped me figure this out.

3. Querysets are lazy

To write efficient queries, it’s important to understand that querysets are lazy.

This means that the line of code below will not trigger any database activity.

queryset = Entry.objects.all()

In fact, adding any filters to it won’t either.

Django does not touch our database till the time the values queried aren’t evaluated. Which happens only when we use the value. We can force an evaluation by converting a queryset to a list or printing a filtered queryset.

Additionally, a cache is created every time a queryset is evaluated for the first time. Later evaluations then reuse this cache to minimise database access.

To take advantage of this, we must reuse querysets wherever possible. Consider the following example:

Snippet 1

>>> print([e.headline for e in Entry.objects.all()])
>>> print([e.pub_date for e in Entry.objects.all()])

Snippet 2

>>> queryset = Entry.objects.all()
>>> print([p.headline for p in queryset]) # Evaluate the query set.
>>> print([p.pub_date for p in queryset]) # Reuse the cache from the evaluation.

In the first snippet, the same database query is executed twice, doubling your database load.

4. Separate config from code

When we create a new project with django-admin, by default the app’s config lives in a file named settings.py.

While many of these config variables are common, or at least similar for most Django apps. Some of the config variables are unique to your application. For e.g. ALLOWED_HOSTS, SECRET_KEY, DATABASES.

I like to think of these as my application’s state and always separate these values from my codebase.

To do this, I use python-decouple. This tool allows us to import our app’s config values from environment variables. Keeping our codebase free from configuration and secrets.

This approach has 2 advantages:

  1. The convenience of being able to update your app without modifying code.

  2. Added security from not committing your project’s secrets to your version control system.

Here’s the article that helped me set this up for my Web Scraping API’s backend.

Interesting side note, separating code from config is an idea I first read on the 12 Factor App Methodoly website. It’s worth checking out if you’re an engineer building a web app for your software-as-a-service.

5. Configure Access Logs

The default format of Gunicorn (or your web server’s) access logs will probably not include all the details you would need.

You can refer to the Gunicorn configuration snippet above, to see how I have modified the value access_log_format to suit my application’s needs.

More info on this can be found here.

6. Beware of the N+1 Problem

When using a Django Rest Framework’s nested serializer within your veiws, by default Django’s ORM creates 1 + n queries to the DB, where n is the number of items returned by the nested serializer.

If several views on your app are using nested serializers, the effect of this could increase the load on your database significantly. You can avoid this by prefetching instances to be returned by the nested serializer.

For a detailed explanation on this, check out this helpful article which also has a great example.

Thanks to the users on Lobsters’ comment section for this additional tip!!

7. Other Recommendations

Since the remaining recommendations are not as nuanced as the ones listed above—I have grouped them all here.

Enable Django sites framework with the setting SITE_ID=1 as recommended in the official docs.

Make sure all you app names and labels in the INSTALLED_APPS list are unique.

Remember to use foreign key values directly as shown here.

Lastly, when deploying for product, don’t forget to set debug as False.


Profile picture

Written by Raunaq Singh who loves Yoga, lives in Mumbai, is building Unwrangle.com & CustomerSense.io and is forever grateful for everything. You should follow them on Twitter