Senior 10 min · March 05, 2026
Django Web Framework Basics

Django N+1 Queries — ORM Patterns That Kill Performance

One missing select_related caused 500+ queries per page load.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Lessons pulled from things that broke in production.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Django is a high-level Python web framework that follows the Model-View-Template (MVT) architectural pattern
  • Models define database schema and business logic; Views handle HTTP request/response; Templates render HTML dynamically
  • URL dispatcher maps URLs to views using regex or path converters
  • Django ORM abstracts SQL, support for migrations, and lazy evaluation of QuerySets
  • Production trap: N+1 queries from unoptimized ORM usage; use select_related and prefetch_related
  • Biggest mistake: treating templates as presentation-only when they often contain business logic in early iterations
✦ Definition~90s read
What is Django Web Framework Basics?

Django's Model-View-Template (MVT) architecture split concerns: models define data and business logic, views handle request/response cycles, and templates present data. Unlike MVC where the controller is separate, Django's view acts as both controller and view.

Imagine you're running a restaurant.

The framework is opinionated — it expects you to follow its conventions. That's not a limitation; it's the reason you can move faster. You'll spend less time deciding how to structure your app and more time building features. The built-in ORM, admin panel, authentication, and security middleware come ready to use, so you don't reach for third-party libraries until you actually need them.

Plain-English First

Imagine you're running a restaurant. The kitchen (your database) stores all the food. The waiter (your view) takes orders from customers and fetches from the kitchen. The menu (your template) is what the customer actually sees. Django is the restaurant management system that connects all three so you don't have to wire every single thing yourself — it handles the routing, the talking to the database, the security checks, and the page rendering, all out of the box.

Every time you sign up for a new account on a website, post a comment, or see a personalised dashboard, there's a web framework doing the heavy lifting behind the scenes. Django is the framework that powers Instagram, Pinterest, Disqus, and Mozilla — not because they couldn't build something custom, but because Django handles the boring, dangerous, and repetitive parts of web development so engineers can focus on the actual product. It's not just popular; it's one of the most battle-hardened Python tools in existence.

Before frameworks like Django existed, developers had to manually parse HTTP requests, write raw SQL queries, sanitise every user input by hand, and invent their own way to map URLs to functions. Forget about one bug — you'd be inviting dozens of security vulnerabilities every time you forgot to escape a string. Django solves this by giving you a structured, opinionated foundation: a built-in ORM so you never write raw SQL by accident, an automatic admin panel, CSRF protection enabled by default, and a URL routing system that scales from a single page to thousands of endpoints.

By the end of this article you'll understand the MVT (Model-View-Template) architecture from first principles, know how to set up a real Django project with a database-backed model, wire up URL routes to views, and render dynamic HTML templates — the four pillars that every Django application is built on. You'll also know the classic mistakes that trip up intermediate developers and exactly how to sidestep them.

What is Django Web Framework Basics?

Django's Model-View-Template (MVT) architecture split concerns: models define data and business logic, views handle request/response cycles, and templates present data. Unlike MVC where the controller is separate, Django's view acts as both controller and view. The framework is opinionated — it expects you to follow its conventions. That's not a limitation; it's the reason you can move faster. You'll spend less time deciding how to structure your app and more time building features. The built-in ORM, admin panel, authentication, and security middleware come ready to use, so you don't reach for third-party libraries until you actually need them.

io/thecodeforge/products/views.pyPYTHON
1
2
3
4
5
6
from django.shortcuts import render
from .models import Product

def product_list(request):
    products = Product.objects.select_related('category').all()
    return render(request, 'products/list.html', {'products': products})
MVT vs MVC
In classic MVC, the controller updates the model and view. In Django, the view does both: it interacts with the model and passes data to the template. The template is the presentation layer only.
Production Insight
The biggest advantage of Django's opinionated structure is consistency across projects.
A junior dev can pick up any Django project and navigate it within days.
Rule: fight the framework's conventions only when you have a measurable reason.
Key Takeaway
Django is a full-featured web framework, not a microframework.
It enforces structure: project, apps, models, views, templates, URLs.
The framework's 'batteries-included' philosophy reduces boilerplate but requires discipline to avoid abuse.
Django ORM Query Optimization Flow THECODEFORGE.IO Django ORM Query Optimization Flow From naive N+1 queries to efficient data loading with select_related and prefetch_related N+1 Query Problem One query per parent plus one per child select_related JOIN for foreign keys and one-to-one prefetch_related Separate query for many-to-many and reverse Decision Matrix Choose based on relationship cardinality Optimized QuerySet Reduced database hits, faster response ⚠ prefetch_related still runs extra queries per table Use select_related for FK/O2O; prefetch_related for M2M/reverse THECODEFORGE.IO
thecodeforge.io
Django ORM Query Optimization Flow
Django Web Framework Basics

Django MTV (Model-Template-View) Architecture Flow

The MTV flow is the core request-response cycle in Django. When a request hits your server, the URL dispatcher (urls.py) matches the path to a view function or class. The view then interacts with the model layer – fetching or updating data via the ORM – and finally passes that data to a template to render the HTML response. This separation keeps each layer focused: models on data, views on logic, templates on presentation. Understanding this flow is critical because production performance issues often arise when views accidentally perform heavy data operations (like N+1 queries) or when templates contain expensive logic that should be in the view.

The diagram below illustrates the request path through the MTV architecture, showing how the URLconf, view, model, and template connect.

Flow optimisation checklist
Profile each step: is the URL slow to resolve? (rare). Is the view making extra queries? (common). Is the template rendering too slow? (use fragment caching).
Production Insight
In high-traffic applications, the view is where most latency originates – often from ORM queries. Profile the view first.
The template rendering step can also become a bottleneck if you use expensive filters or complex inheritance.
Rule: keep views thin (delegate to model methods or services), keep templates logic-free.
Key Takeaway
The MTV flow is a pipeline: URL → View → Model → Template → Response. Each layer has a well-defined responsibility, and cross-layer dependency (e.g., queries in templates) breaks the separation and hides performance issues.
Django MTV Request Flow
HTTP RequestQuery ResultRendered HTMLHTTP ResponseClient Browserurls.pyviews.pymodels.py / ORMtemplates/

Django Project Structure: File Roles and Responsibilities

A Django project consists of a project directory (often containing settings.py, urls.py, wsgi.py, asgi.py, manage.py) and one or more apps. Each app typically has models.py, views.py, urls.py, admin.py, tests.py, and folders for templates, static files, and migrations. Understanding the role of each file helps you navigate any Django project quickly and avoid putting code in the wrong place.

FilePurposeCommon Pitfall
manage.pyCommand-line utility for running development server, migrations, shell, etc.Running commands from outside the project directory. Always run from the project root.
settings.pyAll configuration: database, installed apps, middleware, templates, static files, security keys.Hardcoding secrets or DEBUG=True in production. Use environment variables.
urls.py (project-level)Includes URL patterns from apps and defines global route prefixes.Putting app-specific patterns here instead of in the app’s own urls.py.
wsgi.py / asgi.pyEntry points for WSGI/ASGI servers (Gunicorn, uWSGI, Daphne).Modifying these files incorrectly can break deployment.
App's models.pyDefines database schema and business logic as Python classes.Putting business logic in views instead of model methods.
App's views.pyHandles HTTP requests and returns responses.Overloading views with data processing; keep them thin.
App's urls.pyRoute definitions for the app's endpoints.Forgetting to name patterns (for {% url %}) or ordering patterns incorrectly.
App's admin.pyCustomises the auto-generated admin interface for models.Exposing sensitive fields or allowing bulk deletes without confirmation.
App's migrations/Version-controlled schema changes generated by makemigrations.Manually editing migration files; always create new migrations for schema changes.
App's templates/HTML files with Django template language.Including business logic (e.g., complex conditionals) that should be in views.
App's static/CSS, JavaScript, images.Forgetting to run collectstatic before deployment.
tests.pyUnit tests for the app.Skipping tests — write them for models, views, and forms.
io/thecodeforge/project_tree.txtTEXT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
myproject/
├── manage.py
├── myproject/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   ├── wsgi.py
│   └── asgi.py
├── myapp/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations/
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
└── requirements.txt
App structure is not enforced by Python, only by convention
You can put models anywhere, but following the standard layout makes your project predictable for other developers.
Production Insight
The most common file misplacement in production is putting app-specific URLs in the project-level urls.py instead of the app's own urls.py. This leads to conflicts when multiple apps share similar paths.
Rule: each app owns its URLs; the project urls.py only includes them via include().
Key Takeaway
Django's project structure is convention-based but sensible: settings go in the project root, app logic in apps, with clear file roles. Deviate only when you have a compelling reason.

Django Models and the ORM: Mapping Your Data Without SQL

Models are Python classes that inherit from django.db.models.Model. Each attribute represents a database field — CharField, IntegerField, ForeignKey, etc. Django automatically creates a database table for each model. The ORM translates QuerySet operations like filter(), exclude(), annotate() into SQL queries executed lazily. This means you can chain filters without hitting the database until the result is actually needed. Migrations track changes to the model definitions and sync them to the database schema. The admin interface is generated automatically from models, which is a massive time-saver for prototyping and content management. The real power comes from the ORM's ability to generate optimized SQL, but only if you understand its lazy evaluation and eager loading options.

io/thecodeforge/products/models.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=200)
    description = models.TextField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [
            models.Index(fields=['category']),
            models.Index(fields=['price']),
        ]
        ordering = ['-created_at']

    def __str__(self):
        return self.name

class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)
    slug = models.SlugField(unique=True)

    def __str__(self):
        return self.name
Database Schema as Python Classes
  • A model class maps to one database table
  • Field types (CharField, ForeignKey) define column types and constraints
  • Every model automatically gets an 'id' primary key unless overridden
  • Migrations are version-controlled schema changes; never alter the database manually in production
Production Insight
Lazy evaluation means you can accidentally trigger many queries in a loop.
Use select_related for ForeignKey and OneToOneField joins, prefetch_related for ManyToManyField and reverse relations.
Rule: if you see repeated queries for related objects, apply eager loading immediately.
Key Takeaway
Models define your data shape; migrations keep schema in sync.
Queries are lazy — chain filters freely, but beware of unintended database hits.
Use eager loading to avoid the N+1 query trap in production.
When to Use Select/Preload Related
IfAccessing a single ForeignKey in a loop or template
UseUse select_related with the exact field name
IfAccessing many-to-many or reverse relations
UseUse prefetch_related to issue a second query and cache the result
IfNeed to filter on related fields without data duplication
UseUse Prefetch objects for fine-grained control

Choosing between select_related and prefetch_related is one of the most critical performance decisions in Django ORM. The wrong choice can either cause an N+1 avalanche (missing eager loading) or create unnecessary database load (using a join when two queries are faster). This matrix helps you decide which method to use based on the relationship type and your access pattern.

Decision Factorselect_relatedprefetch_related
Relationship typeForeignKey, OneToOneFieldManyToManyField, reverse ForeignKey, generic relations
SQL executionSingle query with JOIN(s)Separate query per relation, then Python-level joining
Performance trade-offBest for small number of related objects, but can produce large result sets with many joinsBest for large collections or when you need to filter related objects (Prefetch object)
When to useYou always need the related object and it's a single row per parentYou may need a subset of related objects, or the relation is a collection
Danger signYour query returns many duplicate parent rows due to JOIN explosionYou still see N+1 because you didn't apply prefetch_related (it only works if you actually access the related set)

```python # Use select_related for ForeignKey products = Product.objects.select_related('category').all() for product in products: print(product.category.name) # No extra query

# Use prefetch_related for ManyToManyField categories = Category.objects.prefetch_related('products').all() for category in categories: print(category.products.all()) # Already prefetched

# For advanced prefetch filtering from django.db.models import Prefetch categories = Category.objects.prefetch_related( Prefetch('products', queryset=Product.objects.filter(price__gt=10)) ).all() ```

io/thecodeforge/products/views_optimized.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# select_related example
from django.shortcuts import render
from .models import Product

def product_list(request):
    # Single JOIN query
    products = Product.objects.select_related('category').all()
    return render(request, 'products/list.html', {'products': products})

# prefetch_related example
from .models import Category

def category_list(request):
    # Two queries: one for categories, one for products
    categories = Category.objects.prefetch_related('product_set').all()
    return render(request, 'categories/list.html', {'categories': categories})
prefetch_related does not prevent queries if you never access the related set
Django still issues the separate query even if you never iterate over the related objects. Use Prefetch with to_attr to store results in a list, then check if the list is empty.
Production Insight
In production, start with select_related for all ForeignKey fields you access. Then profile: if you see duplicate rows due to JOIN, switch to prefetch_related or use .distinct() carefully.
Monitor query count using django-debug-toolbar; a single page should rarely exceed 20 queries.
Rule: always eager-load any relation you access in a loop or in a template.
Key Takeaway
select_related does a JOIN and works for single-object relations; prefetch_related does a separate query and works for collections. Use the decision matrix to pick the right one and always profile your queries.

URL Routing and Views: Mapping Requests to Responses

Django's URL dispatcher uses a URLconf — a Python module (usually urls.py) that maps URL patterns to view functions or class-based views. Each pattern is defined with path() or re_path(). Path converters (<int:pk>, <slug:slug>) extract typed parameters from the URL. Views receive an HttpRequest object and return an HttpResponse. Function-based views are simple and explicit; class-based views (CreateView, ListView, DetailView) reduce boilerplate for common CRUD operations. Middleware processes request and response globally — common uses include authentication, CSRF protection, and session management. The order of patterns matters; Django stops at the first match, so place specific patterns before generic ones. Use named URL patterns to avoid hardcoding links.

io/thecodeforge/products/urls.pyPYTHON
1
2
3
4
5
6
7
8
9
from django.urls import path
from . import views
from .views import ProductUpdateView

urlpatterns = [
    path('', views.product_list, name='product-list'),
    path('<int:pk>/', views.product_detail, name='product-detail'),
    path('<int:pk>/update/', ProductUpdateView.as_view(), name='product-update'),
]
Production Insight
URL pattern order matters — Django stops at the first match.
A catch-all pattern (e.g., '<slug:slug>') placed too early can shadow all other routes.
Rule: place specific patterns first, generic ones last.
Key Takeaway
URLs are Python functions or classes; keep them thin.
Class-based views save time but can hide complexity — read their method resolution order.
Middleware executes on every request; be careful with expensive operations.

Django Templates: Dynamic HTML Without Spaghetti

Django's template engine renders HTML files with variable substitution, template tags ({% for %}, {% if %}), and filters ({{ value|date:'Y-m-d' }}). Templates inherit from base templates using {% extends %} and blocks {% block content %}{% endblock %}. This promotes DRY design across pages. The built-in template system is sandboxed — it prevents arbitrary Python execution. For heavy logic, use custom template tags or filters, but avoid putting business logic in templates to preserve separation of concerns. Template fragment caching with the {% cache %} tag reduces rendering time for expensive blocks.

io/thecodeforge/products/templates/products/list.htmlDJANGO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{% extends 'base.html' %}
{% block content %}
  <h1>Products</h1>
  <ul>
    {% for product in products %}
      <li>
        <a href="{% url 'product-detail' product.pk %}">{{ product.name }}</a>
        - ${{ product.price|floatformat:2 }}
        ({{ product.category.name }})
      </li>
    {% empty %}
      <li>No products yet.</li>
    {% endfor %}
  </ul>
{% endblock %}
Template Performance
Django caches parsed templates in production. Use template fragment caching with the {% cache %} tag for expensive blocks that rarely change.
Production Insight
Accessing related fields in a loop inside a template triggers the same N+1 query problem.
Preload all needed related data in the view before passing context to the template.
Rule: never call model methods that generate queries inside template {% for %} loops.
Key Takeaway
Templates present data — they should not derive it.
Use inheritance and includes to avoid duplication.
Keep template logic minimal; put computation in views or custom filters.

Django Middleware and the Request/Response Cycle

Middleware is a lightweight plugin system that processes requests before they reach the view and responses before they return to the client. Each middleware component is a Python class or function following a specific interface. Common built-in middleware: SecurityMiddleware (HTTPS redirect, HSTS), SessionMiddleware, AuthenticationMiddleware, CSRFMiddleware. Custom middleware can be added for logging, metrics, IP blocking, or modifying headers. The middleware chain is defined in setting MIDDLEWARE (order matters: request goes top-down, response goes bottom-up). Each middleware should be fast and stateless because it runs on every request.

io/thecodeforge/core/middleware.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import time
from django.http import HttpResponse

class RequestTimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        start = time.time()
        response = self.get_response(request)
        duration = time.time() - start
        response['X-Request-Duration-ms'] = int(duration * 1000)
        # In production, log this to metrics
        return response
Production Insight
Middleware also sees static file requests (if not served by the web server).
Heavy middleware (e.g., authentication) can become a bottleneck under load.
Rule: place security middleware towards the top, performance/caching middleware later.
Key Takeaway
Middleware runs on every request — design it to be fast and stateless.
Order matters: request flows top-down, response flows bottom-up.
Use middleware for cross-cutting concerns, not per-view logic.

Django Forms and Validation

Django forms handle HTML form rendering, validation, and cleaning. A Form class defines fields with built-in validators (required, max_length, regex). ModelForm automatically generates a form from a model. In the view, you instantiate the form with request.POST or request.FILES, call is_valid(), and access cleaned_data. Validation happens server-side; never trust client-side validation alone. Forms also handle CSRF tokens automatically. Custom validators can be added per field or per form.

io/thecodeforge/products/forms.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from django import forms
from .models import Product

class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = ['name', 'description', 'price', 'category']
        widgets = {
            'description': forms.Textarea(attrs={'rows': 3}),
        }

    def clean_price(self):
        price = self.cleaned_data.get('price')
        if price and price <= 0:
            raise forms.ValidationError('Price must be positive')
        return price
Client-side validation is not enough
JavaScript validation is for user experience. Always enforce validation on the server with Django forms. An attacker can bypass client-side checks.
Production Insight
Form validation can trigger additional database queries if not careful.
Use select_related on related fields used in validation (e.g., checking uniqueness).
Rule: keep validation logic in the form, not the view.
Key Takeaway
Forms handle rendering, validation, and cleaning.
ModelForm cuts boilerplate by mapping directly to model fields.
Always validate on the server — client-side is convenience, not security.

Django Admin Customisation

The Django admin interface is generated automatically from your models. You can customise it extensively: list_display shows columns, search_fields enables search, list_filter adds sidebar filters, date_hierarchy adds date drill-down, and actions add bulk operations. Inlines display related models on the same page. The admin is intended for staff users, not end customers. It's great for content management and internal tools. Avoid putting business logic in admin methods; instead, write management commands or separate views.

io/thecodeforge/products/admin.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.contrib import admin
from .models import Product, Category

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ['name', 'price', 'category', 'created_at']
    list_filter = ['category', 'created_at']
    search_fields = ['name', 'description']
    date_hierarchy = 'created_at'
    ordering = ['-created_at']

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    prepopulated_fields = {'slug': ('name',)}
Production Insight
Do not expose the admin to end users — it's not designed for scaling or UX.
If the admin becomes slow, check for unindexed fields in list_editable or large querysets.
Rule: customise admin for staff, build separate interfaces for customers.
Key Takeaway
Admin is automatic but customisable.
Use list_display, search_fields, list_filter to improve productivity.
Admin is for staff, not for end users.

Django Security Best Practices

Django includes built-in protections for common web vulnerabilities: CSRF tokens on all POST forms, XSS escaping in templates, SQL injection via parameterised ORM queries, clickjacking via X-Frame-Options, and HTTPS redirect via SecurityMiddleware. But these are not automatic if misconfigured. In production, always set DEBUG=False, rotate SECRET_KEY, restrict ALLOWED_HOSTS, use HTTPS with SECURE_SSL_REDIRECT, set CSRF_COOKIE_SECURE and SESSION_COOKIE_SECURE. Use django-csp for Content Security Policy headers. Keep Django and dependencies updated to patch known vulnerabilities.

io/thecodeforge/core/settings.pyPYTHON
1
2
3
4
5
6
7
8
9
10
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com']
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
X_FRAME_OPTIONS = 'DENY'
Secret key exposure
Never commit SECRET_KEY to version control. Use environment variables or a secrets manager. If leaked, an attacker can forge session cookies and signatures.
Production Insight
Django's defaults are secure but not sufficient for all threat models.
Common omissions: missing HSTS, not setting CSRF cookie secure, using HTTP in staging.
Rule: run python manage.py check --deploy before going live.
Key Takeaway
Django provides strong security defaults, but they must be configured correctly.
Use check --deploy to audit settings.
Keep dependencies updated.

Why Django? The Production Reality Check

Django ships with everything you need to build a web application that won't implode under moderate load. The admin panel isn't a toy—it's a full CRUD interface you can hand to non-technical stakeholders on day one. The ORM prevents SQL injection by default, and CSRF tokens are baked into every form. You don't waste time wiring up authentication, sessions, or migrations. The trade-off: Django makes opinionated choices about project structure. That's a feature when you onboard junior devs or switch projects. The DRY principle means you define a model once and the admin, forms, and views all inherit that schema. Scaling beyond a single server? Add caching, a message queue, and database read replicas. Django won't stop you—it just won't do it for you.

Production Trap:
Do NOT modify the default User model directly. Extend it via AbstractUser or OneToOneField. Changing it mid-project is a migration nightmare that will lock your database.
Key Takeaway
Django's built-in admin and ORM are production-grade; use them, but never fight the framework's conventions.

QuerySets: Your SQL Without the Pain (or the Leaks)

A QuerySet is a lazy evaluation of a database query. You chain filters, excludes, and annotations, but nothing hits the database until you iterate, slice, or call .count() or .exists(). This laziness is why select_related and prefetch_related matter—they let you grab related objects in one or two queries instead of N+1. Danger zone: slicing a QuerySet with [0] triggers a LIMIT 1, but it also evaluates the entire set if you chain it after an annotation. Always use .first() or .last() for a single object. Never evaluate a QuerySet in a loop without understanding the cache: iterating once caches the results, but a second iteration on the same object uses the cache. Ignore this, and you'll fire a query per iteration in a template loop.

queries.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# io.thecodeforge
from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    published = models.DateField()

# Lazy: no DB hit yet
recent_books = Book.objects.filter(published__year=2024)

# Eager load author to avoid N+1
books_with_authors = Book.objects.select_related('author').filter(published__year=2024)

# Only now does the DB get hit
for book in books_with_authors:
    print(f"{book.title} by {book.author.name}")
Output
Harry Potter and the Philosopher's Stone by J.K. Rowling
The Hobbit by J.R.R. Tolkien
Production Trap:
If you use .iterator() on a QuerySet to reduce memory, Django doesn't cache results. Iterate twice, and you'll hit the database twice. Use .iterator() only for one-pass operations like bulk exports.
Key Takeaway
QuerySets are lazy; use select_related and prefetch_related to prevent N+1 queries, and never iterate a QuerySet twice without caching.

Authentication and Authorization: Don't Roll Your Own

Django's authentication system handles login, logout, password resets, and session management out of the box. Use django.contrib.auth for User models, permissions, and groups. For REST APIs, pair it with Django REST Framework's TokenAuthentication or JWT. The WHY: rolling custom auth means you'll miss edge cases like password hashing algorithms, brute-force protection, or session fixation. Django's auth middleware adds request.user to every view. Use @login_required decorator or LoginRequiredMixin to protect views. For fine-grained control, use @permission_required or check user.has_perm() in views. Never store plaintext passwords—Django uses PBKDF2 by default. If you need OAuth, use django-allauth. It integrates with Google, GitHub, and dozens of providers without you writing a single token exchange.

Production Trap:
When customizing the User model, do it before the first migration. Django's auth framework expects auth.User by default. Changing it later requires a complex data migration and potential downtime.
Key Takeaway
Use Django's built-in auth for sessions and permissions; extend with django-allauth or DRF for OAuth and APIs.
● Production incidentPOST-MORTEMseverity: high

N+1 Query Meltdown on a Busy Product Page

Symptom
Slow page rendering under load, high database connection count, excessive query logs showing repeated identical SELECT statements for related data.
Assumption
Team assumed Django ORM lazily loads related objects efficiently; they didn't realize accessing a ForeignKey field on each item triggers a separate query.
Root cause
Lack of eager loading: the view iterated over products and accessed product.category.name, causing one additional query per product (the classic N+1).
Fix
Use select_related('category') in the queryset to JOIN the category table in a single query. For many-to-many relations, use prefetch_related.
Key lesson
  • Always examine the SQL queries Django generates during development (django.db.connection.queries or django-debug-toolbar).
  • If you see repeated queries in a loop, you have an N+1 problem.
  • Eager loading is the first performance optimization to apply before caching.
Production debug guideSymptom → Action diagnostic patterns for common Django failures4 entries
Symptom · 01
404 errors for pages that should exist
Fix
Check URL patterns in urls.py — look for missing path converters or reordered patterns that shadow later ones. Run python manage.py show_urls (using django-extensions) to list all resolved URLs.
Symptom · 02
500 errors after a migration
Fix
Verify the migration was applied on the target database. Check for column mismatches between models and the actual schema. Use python manage.py sqlmigrate <app> <migration_id> to see the generated SQL.
Symptom · 03
Slow queries increasing page latency
Fix
Enable django-debug-toolbar in a staging environment and reproduce the slow page. Look for the number of queries and repeated queries. Apply select_related, prefetch_related or index missing fields.
Symptom · 04
CSRF token missing or incorrect errors
Fix
Ensure every POST form includes {% csrf_token %}. Verify the SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE settings are correct for HTTPS. Check for any custom middleware that might strip the CSRF cookie.
★ Quick Django Debug Cheat SheetFirst commands to run when something breaks in a Django production app
Page returns 500 error with no details
Immediate action
Check DEBUG=False log files (Apache/Nginx error log, or Django's setting LOGGING). If no logs, temporarily set DEBUG=True in a non-production environment to reproduce.
Commands
curl -I https://yoursite.com/slow-page
tail -n 100 /var/log/django/error.log
Fix now
Set LOGGING level to DEBUG for django.request logger in settings.py.
Database queries are slow+
Immediate action
Enable slow query logging in the database. In PostgreSQL: set log_min_duration_statement = 200ms.
Commands
python manage.py shell -c "from django.db import connection; print(connection.queries)"
python manage.py showmigrations
Fix now
Add database indexes to columns used in WHERE, JOIN, and ORDER BY clauses.
Migration conflicts after multiple developers+
Immediate action
Merge migration files manually by creating a new empty migration that depends on both conflicting migrations.
Commands
python manage.py makemigrations --merge
python manage.py migrate --fake-initial
Fix now
Run makemigrations --merge and then migrate to apply the merged migration.
Django vs Other Python Web Frameworks
FeatureDjangoFlaskFastAPI
ArchitectureMVT (full-stack)Minimal, flexibleASGI, async-first
Built-in ORMYes (rich, with migrations)No (SQLAlchemy common)No (SQLAlchemy, Tortoise)
Admin PanelAutomatic from modelsExtensions neededThird-party solutions
REST APIDjango REST Framework (extensive)Flask-RESTful or manualBuilt-in Pydantic validation
Async SupportLimited (via Channels or async views since 3.0)None (WSGI only, until Quattro)Native async from the start
Learning CurveModerate (opinionated, many conventions)Low (minimal boilerplate)Moderate (fast if you know Python types)
Best ForLarge monolithic apps, CMS, rapid prototypingMicroservices, small APIs, prototypesHigh-performance APIs, real-time services

Key takeaways

1
Django's MVT architecture separates data (models), logic (views), and presentation (templates) but requires discipline to keep them truly separated.
2
The ORM eliminates raw SQL for 90% of queries but introduces N+1 performance traps
always inspect generated queries.
3
URL routing is explicit; pattern order matters and path converters prevent regex debugging nightmares.
4
Templates inherit and compose; keep logic out of templates and use custom filters/tags for presentation logic.
5
Middleware provides global request/response hooks; use it sparingly and ensure each middleware is fast and side-effect-free.
6
Common production pitfalls include missing migrations, lazy query evaluation, and misconfigured CSRF/security settings.

Common mistakes to avoid

5 patterns
×

Not using select_related and prefetch_related

Symptom
Page becomes slow as data grows; database query count skyrockets; server CPU spikes under moderate traffic.
Fix
Identify loops accessing related objects and add select_related (ForeignKey) or prefetch_related (ManyToMany, reverse) to the queryset in the view.
×

Overusing the admin site for everything

Symptom
Admin pages become slow, complex, or expose too much data; confusion between admin interface and user-facing UI.
Fix
Customise the admin site (list_display, search_fields, list_filter), or build separate staff-facing views using Django's form and CRUD capabilities.
×

Putting business logic in templates or views

Symptom
Hard to test or reuse; templates become cluttered; views grow unreadable.
Fix
Move business logic to model methods, managers, or service layer classes outside views/templates.
×

Ignoring database indexing

Symptom
Simple queries become slow as table grows; database CPU increases; EXPLAIN shows full table scans.
Fix
Add indexes to fields used in WHERE, JOIN, ORDER BY, and unique constraints. Use Meta.indexes in models or run RunSQL migrations.
×

Misunderstanding CSRF protection and cookie settings

Symptom
Users getting CSRF token errors on forms; login cookies not set correctly on HTTPS.
Fix
Ensure CSRF_COOKIE_SECURE and SESSION_COOKIE_SECURE are True in production (HTTPS). Use {% csrf_token %} in every POST form. Do not disable CSRF globally.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the difference between Django's ORM and raw SQL. When would you ...
Q02JUNIOR
How does Django's middleware work? Give an example of custom middleware ...
Q03SENIOR
What is the Django request/response cycle? Describe the path a request t...
Q04SENIOR
Explain the difference between select_related and prefetch_related. When...
Q05SENIOR
How do you handle database migrations in a production environment with m...
Q01 of 05SENIOR

Explain the difference between Django's ORM and raw SQL. When would you still write raw SQL in Django?

ANSWER
The ORM provides an abstraction: Python classes map to tables, QuerySet methods generate SQL. It handles cross-database compatibility and injects parameterised queries to prevent SQL injection. However, raw SQL becomes useful for complex reports, window functions, CTEs, or performance-critical queries where the ORM generates suboptimal SQL. Django provides RawSQL and raw() methods for these cases, but they should be used sparingly and reviewed carefully.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is Django Web Framework in simple terms?
02
What is the difference between Django and Flask?
03
How do I debug Django's ORM queries?
04
What is the difference between function-based views and class-based views?
05
How do I secure a Django app in production?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Lessons pulled from things that broke in production.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's Python Libraries. Mark it forged?

10 min read · try the examples if you haven't

Previous
Flask Web Framework Basics
9 / 51 · Python Libraries
Next
SQLAlchemy Basics