Why Your Django App Is Slow (And It’s Not Django’s Fault)
Comments
Sign in to join the conversation
Sign in to join the conversation
When a Django application starts feeling sluggish, the immediate instinct is to blame the framework. "Python is slow," or "Django is too heavy," are common complaints.
In reality, 90% of performance issues stem from how we use the framework, not the framework itself. A raw request in Django takes milliseconds. If your page takes 2 seconds to load, that time is being spent in your code, your database, or your network—not inside Django's core.
Here is a breakdown of the request lifecycle, where the time actually goes, and how to fix it.
To fix speed, you must visualize the path of a request.
If your app is slow, it is almost certainly in step 4 (View Logic) or 5 (Rendering).
The #1 cause of slowness is treating the database like a local dictionary.
You might think you are running one query, but your serializer or template is running hundreds.
The Slow Way (N+1 Problem):
# View
def list_books(request):
books = Book.objects.all() # 1 Query
return render(request, 'books.html', {'books': books})
# Template (books.html)
# The loop below triggers a NEW QUERY for every single book
# because it accesses the foreign key 'author'.
#
# {% for book in books %}
# {{ book.author.name }}
# {% endfor %}
If you have 500 books, you just ran 501 queries. The view finished fast, but the rendering step took forever because it had to talk to the database 500 times.
The Fix:
# Fetch the author data in the initial JOIN
books = Book.objects.select_related('author').all()
If you are building an API with Django REST Framework (DRF), serialization is often the slowest part of your stack. DRF is powerful, but instantiating thousands of Python objects is expensive.
Using SerializerMethodField is convenient, but it runs a Python function for every single row.
class BookSerializer(serializers.ModelSerializer):
# This runs a separate DB query or complex logic for every book in the list
is_best_seller = serializers.SerializerMethodField()
def get_is_best_seller(self, obj):
return obj.sales_count > 1000
The Fix: Annotate the data in the database, so the serializer just reads a value.
# View
books = Book.objects.annotate(
is_best_seller=Case(
When(sales_count__gt=1000, then=Value(True)),
default=Value(False),
output_field=BooleanField()
)
)
# Serializer
# Just map the field directly, no python logic needed
is_best_seller = serializers.BooleanField(read_only=True)
Middleware runs on every single request. If you put a heavy API call or a complex database check in your middleware, you slow down your entire application, even for static files or health checks.
Bad Middleware Example:
class UserTrackingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# BAD: Updates the DB on every single hit (CSS, JS, Images, API)
UserProfile.objects.filter(user=request.user).update(last_seen=now())
return self.get_response(request)
The Fix:
if request.path.startswith('/api/')).Stop guessing. You cannot fix what you cannot see.
Use for: Local development. It shows you the exact SQL queries, how long they took, and which line of code triggered them. If you see a bar with "500 Queries", you found your problem.
Use for: Staging/Local profiling. Silk intercepts requests and records:
Configure Django to log any query that takes longer than X seconds. This helps you catch production issues.
# settings.py
LOGGING = {
'loggers': {
'django.db.backends': {
'level': 'DEBUG',
'handlers': ['console'],
},
},
}
Django is rarely the bottleneck; your usage of it is. The most common performance killers are N+1 database queries during template/serializer rendering, heavy Python logic in serializers (like SerializerMethodField), and inefficient middleware that blocks every request. To fix this, move logic from Python to the database using annotate and select_related, and use profiling tools like Django Debug Toolbar to visualize where the time is actually being lost.