Introduction to Django’s Request Pipeline

This article introduces Django’s request pipeline and explains how an HTTP request moves through the web server, WSGI or ASGI, middleware, URL resolution, views, forms, models, templates, and back as an HTTP response. It also covers authentication, sessions, CSRF, redirects, errors, static and media requests, debugging, and common request-handling mistakes.

Introduction to Django’s Request Pipeline

When a browser opens a Django page, a surprising amount of work happens before HTML appears on the screen.

A request such as:

text

1
GET /articles/42/

does not go directly to a template.

Instead, it moves through several layers of Django.

A simplified request pipeline looks like this:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Browser
   ↓
Web server
   ↓
Django
   ↓
Middleware
   ↓
URL resolver
   ↓
View
   ↓
Models / services / templates
   ↓
HttpResponse
   ↓
Middleware
   ↓
Web server
   ↓
Browser

Understanding this pipeline makes many Django concepts easier to connect.

It helps explain:

  • where request comes from
  • how URLs choose views
  • when middleware runs
  • how authentication reaches a view
  • where database queries happen
  • how templates become HTML
  • how redirects and errors are returned
  • why some behavior affects every request

What Is an HTTP Request?

A browser communicates with a web application using HTTP.

A request contains information such as:

  • HTTP method
  • URL
  • headers
  • cookies
  • query parameters
  • submitted form data
  • uploaded files
  • request body

A simple request may look conceptually like:

text

1
2
3
4
GET /articles/?page=2 HTTP/1.1
Host: example.com
Cookie: sessionid=...
User-Agent: ...

Django converts the incoming request into an HttpRequest object.

The view receives that object as its first argument:

python

1
2
def article_list(request):
    ...

The Request and Response Cycle

At a high level, Django receives a request and must return a response.

text

1
2
3
4
5
Request
   ↓
Django application
   ↓
Response

A function-based view demonstrates this directly:

python

1
2
3
4
5
from django.http import HttpResponse


def hello(request):
    return HttpResponse("Hello")

The input is:

python

1
request

The output is:

python

1
HttpResponse

Almost everything in Django’s request pipeline exists to help determine what should happen between those two points.

The Web Server Receives the Request

In production, Django is normally not the first process that receives an HTTP request.

A request may first reach:

  • Nginx
  • Apache
  • a cloud load balancer
  • a reverse proxy
  • a platform router
  • another web server

The web server may handle things such as:

  • HTTPS
  • static files
  • request buffering
  • compression
  • proxy headers
  • connection management

Dynamic application requests are then passed to Django through an application server.

A production architecture might look like:

text

1
2
3
4
5
6
7
Browser
   ↓
Nginx
   ↓
Gunicorn
   ↓
Django

or:

text

1
2
3
4
5
6
7
Browser
   ↓
Cloud load balancer
   ↓
ASGI server
   ↓
Django

During local development:

bash

1
python manage.py runserver

provides a development server so these external pieces are not usually needed.

WSGI and ASGI

Django applications can run through two main interfaces:

text

1
2
WSGI
ASGI

WSGI is the traditional synchronous Python web-server interface.

ASGI supports asynchronous application behavior as well as traditional synchronous requests.

A Django project normally contains:

text

1
2
3
config/
├── asgi.py
└── wsgi.py

These files expose the Django application to the application server.

Conceptually:

text

1
2
3
4
5
HTTP server
   ↓
WSGI or ASGI
   ↓
Django application

Most beginner Django code does not need to interact directly with these files.

Django Creates an HttpRequest

Once Django receives the request, it creates an HttpRequest object.

A view might inspect:

python

1
2
3
4
def article_list(request):
    print(request.method)
    print(request.path)
    print(request.GET)

Common request attributes include:

text

1
2
3
4
5
6
7
8
9
request.method
request.path
request.GET
request.POST
request.FILES
request.COOKIES
request.headers
request.user
request.session

For example:

python

1
request.method

may contain:

text

1
GET

or:

text

1
POST

Query Parameters

Consider:

text

1
/articles/?page=2&sort=newest

Django places the query-string parameters in:

python

1
request.GET

Example:

python

1
2
page = request.GET.get("page")
sort = request.GET.get("sort")

The name request.GET does not mean it contains all data from every GET request.

It specifically contains query-string parameters.

Submitted Form Data

For a normal POST form:

html

1
<form method="post">

submitted values are commonly available through:

python

1
request.POST

Example:

python

1
title = request.POST.get("title")

In a normal Django application, forms should usually validate this input instead of reading raw values directly:

python

1
2
3
4
form = ArticleForm(request.POST)

if form.is_valid():
    title = form.cleaned_data["title"]

Uploaded Files

Files submitted through:

html

1
2
3
4
<form
    method="post"
    enctype="multipart/form-data"
>

appear in:

python

1
request.FILES

Example:

python

1
uploaded_file = request.FILES["document"]

Django keeps ordinary form data and uploaded files separate.

Middleware Enters the Pipeline

Before the request reaches the view, it passes through Django middleware.

Middleware is code that can inspect or modify requests and responses globally.

The middleware configuration is stored in settings.py:

python

1
2
3
4
5
6
7
8
9
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

Middleware can affect many or all requests.

Examples include:

  • sessions
  • authentication
  • CSRF protection
  • security headers
  • messages
  • redirects
  • logging

Middleware Order Matters

Middleware is ordered.

Conceptually, incoming requests move down the list:

text

1
2
3
4
5
6
7
Middleware A
   ↓
Middleware B
   ↓
Middleware C
   ↓
View

Responses move back through the middleware in reverse:

text

1
2
3
4
5
6
7
View
   ↓
Middleware C
   ↓
Middleware B
   ↓
Middleware A

This creates an onion-like structure:

text

1
2
3
4
5
6
7
A(
    B(
        C(
            view()
        )
    )
)

Because of this, middleware order can affect application behavior.

A Simple Middleware Example

A basic custom middleware might look like:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class RequestLogMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        print(
            "Request:",
            request.method,
            request.path,
        )

        response = self.get_response(request)

        print(
            "Response:",
            response.status_code,
        )

        return response

The flow is:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Receive request
   ↓
Run code before get_response()
   ↓
Pass request deeper
   ↓
Receive response
   ↓
Run code after get_response()
   ↓
Return response

Middleware Can Return a Response Early

Middleware does not always need to allow the request to reach the view.

It can return a response immediately.

Conceptually:

python

1
2
3
4
5
6
7
8
def __call__(self, request):
    if request.path == "/blocked/":
        return HttpResponse(
            "Blocked",
            status=403,
        )

    return self.get_response(request)

In that case:

text

1
2
3
4
5
Request
   ↓
Middleware
   ↓
Response

The URL resolver and view may never run.

This is useful for behavior such as:

  • access restrictions
  • redirects
  • rate limiting
  • maintenance mode

Session Middleware

Django’s session middleware gives the request access to:

python

1
request.session

Example:

python

1
request.session["theme"] = "dark"

Later:

python

1
theme = request.session.get("theme")

Without the session middleware, normal session functionality would not be attached to the request.

Authentication Middleware

Authentication middleware connects the current user to:

python

1
request.user

Example:

python

1
2
3
def dashboard(request):
    if request.user.is_authenticated:
        ...

The authentication system uses session information to determine which user is associated with the request.

Conceptually:

text

1
2
3
4
5
6
7
Request
   ↓
SessionMiddleware
   ↓
AuthenticationMiddleware
   ↓
request.user

This is one reason middleware ordering matters.

CSRF Middleware

For state-changing requests such as many POST submissions, Django’s CSRF middleware checks for a valid CSRF token.

Template:

django

1
2
3
4
<form method="post">
    {% csrf_token %}
    ...
</form>

The request passes through:

text

1
CsrfViewMiddleware

before normal view processing succeeds.

If the token is missing or invalid, Django may return a 403 response before the intended view completes.

URL Resolution

After request middleware processing, Django determines which view should handle the requested path.

Suppose the request is:

text

1
/articles/42/

The project URL configuration may contain:

python

1
2
3
4
5
6
7
8
9
from django.urls import include, path


urlpatterns = [
    path(
        "articles/",
        include("articles.urls"),
    ),
]

Then articles/urls.py might contain:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from django.urls import path

from .views import article_detail


urlpatterns = [
    path(
        "<int:pk>/",
        article_detail,
        name="article-detail",
    ),
]

Django matches:

text

1
/articles/42/

and determines:

text

1
2
3
4
5
view:
    article_detail

keyword arguments:
    pk = 42

URL Patterns Are Checked in Order

Django examines URL patterns from top to bottom.

For example:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
urlpatterns = [
    path(
        "articles/new/",
        article_create,
    ),
    path(
        "articles/<slug:slug>/",
        article_detail,
    ),
]

The specific route is placed first.

If broad routes are placed before more specific routes, unexpected matches may occur.

URL Converters

Django URL patterns can convert path values.

Example:

python

1
2
3
4
path(
    "articles/<int:pk>/",
    article_detail,
)

The converter:

text

1
<int:pk>

means the view receives an integer:

python

1
2
def article_detail(request, pk):
    ...

Other common converters include:

text

1
2
3
4
5
str
int
slug
uuid
path

The View Is Called

Once Django resolves the URL, it calls the selected view.

Function-based view:

python

1
2
def article_detail(request, pk):
    ...

The view receives:

text

1
2
request
pk

It is responsible for eventually producing an HTTP response.

For example:

python

1
2
3
4
5
6
7
from django.http import HttpResponse


def article_detail(request, pk):
    return HttpResponse(
        f"Article {pk}"
    )

Class-Based Views

With a class-based view:

python

1
2
3
4
5
6
from django.views import View


class ArticleView(View):
    def get(self, request, pk):
        ...

the URL uses:

python

1
2
3
4
path(
    "<int:pk>/",
    ArticleView.as_view(),
)

as_view() creates a callable that Django can use like a normal view.

It then dispatches the request based on the HTTP method.

Conceptually:

text

1
2
3
4
5
6
7
GET
   ↓
get()

POST
   ↓
post()

The View Coordinates Application Logic

A view usually coordinates other parts of the application rather than doing everything itself.

For example:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.shortcuts import (
    get_object_or_404,
    render,
)

from .models import Article


def article_detail(request, pk):
    article = get_object_or_404(
        Article,
        pk=pk,
        is_published=True,
    )

    return render(
        request,
        "articles/article_detail.html",
        {
            "article": article,
        },
    )

This view:

  1. receives the request
  2. queries the model
  3. handles a missing object
  4. chooses a template
  5. supplies context data
  6. returns a response

Database Queries

A view may use Django’s ORM:

python

1
2
3
article = Article.objects.get(
    pk=pk,
)

Conceptually:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
View
   ↓
Django ORM
   ↓
Database driver
   ↓
Database
   ↓
Query result
   ↓
Model instance

The browser never communicates directly with the database.

The request passes through Django application logic first.

QuerySets Are Often Lazy

Consider:

python

1
2
3
articles = Article.objects.filter(
    is_published=True,
)

This creates a QuerySet.

The SQL query may not execute immediately.

It commonly executes when Django actually needs the results, such as during:

python

1
list(articles)

iteration:

python

1
2
for article in articles:
    ...

or template rendering:

django

1
2
3
{% for article in articles %}
    ...
{% endfor %}

This matters when reasoning about where database work occurs during the request pipeline.

Service Functions

Larger applications may move business operations out of views.

Instead of:

python

1
2
3
4
5
6
7
def checkout(request):
    # Validate cart.
    # Update inventory.
    # Create order.
    # Charge payment.
    # Send notifications.
    ...

a view might call:

python

1
2
3
4
order = create_order(
    user=request.user,
    cart=cart,
)

The view remains responsible for HTTP concerns:

text

1
2
3
4
Request
Validation
Authentication
Response

while service functions handle application workflows.

Forms in the Request Pipeline

Forms often sit between incoming POST data and business logic.

Example:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def article_create(request):
    if request.method == "POST":
        form = ArticleForm(
            request.POST,
        )

        if form.is_valid():
            article = form.save()

            return redirect(
                "article-detail",
                pk=article.pk,
            )
    else:
        form = ArticleForm()

    return render(
        request,
        "articles/article_form.html",
        {
            "form": form,
        },
    )

The flow is:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
POST request
   ↓
request.POST
   ↓
ArticleForm
   ↓
Validation
   ↓
cleaned_data
   ↓
Model save
   ↓
Redirect response

Authentication and Permissions

A view may check whether the current user is allowed to continue.

Example:

python

1
2
3
4
5
6
7
8
from django.contrib.auth.decorators import (
    login_required,
)


@login_required
def dashboard(request):
    ...

Conceptually:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Request
   ↓
Authentication middleware
   ↓
request.user
   ↓
@login_required
   ↓
Authenticated?
   ├── No → Redirect
   └── Yes → View

Permissions may add another check:

python

1
2
3
4
if not request.user.has_perm(
    "articles.change_article"
):
    ...

Shortcuts Can Return Responses

Django provides several shortcuts that simplify common pipeline operations.

render():

python

1
2
3
4
5
6
7
return render(
    request,
    "articles/list.html",
    {
        "articles": articles,
    },
)

creates an HTTP response from a template.

redirect():

python

1
2
3
return redirect(
    "article-list",
)

creates a redirect response.

get_object_or_404():

python

1
2
3
4
article = get_object_or_404(
    Article,
    pk=pk,
)

raises a 404 condition when an object is missing.

Template Rendering

A view often passes data into a template.

python

1
2
3
4
5
6
7
return render(
    request,
    "articles/article_detail.html",
    {
        "article": article,
    },
)

Template:

django

1
2
3
4
5
<h1>{{ article.title }}</h1>

<p>
    {{ article.content }}
</p>

The template engine combines:

text

1
2
3
4
5
Template
    +
Context
    ↓
Rendered HTML

For example:

html

1
2
3
4
5
<h1>Django Request Pipeline</h1>

<p>
    Example content
</p>

The rendered HTML becomes the body of an HTTP response.

Context Processors

Some template variables are made available through context processors.

For example, depending on configuration, templates may access:

django

1
{{ user }}

or:

django

1
{{ request }}

without every view manually adding them.

Conceptually:

text

1
2
3
4
5
View context
    +
Context processors
    ↓
Template context

Context processors are another example of framework-level behavior that participates indirectly in the request pipeline.

HttpResponse

Every successful Django view ultimately produces an HTTP response.

Basic response:

python

1
2
3
4
5
6
from django.http import HttpResponse


return HttpResponse(
    "Hello"
)

A response contains information such as:

  • status code
  • headers
  • cookies
  • response body

Example:

python

1
2
3
4
response = HttpResponse(
    "Created",
    status=201,
)

Common Response Types

Django provides several response classes and helpers.

Normal response:

python

1
HttpResponse(...)

JSON:

python

1
2
3
4
5
6
7
8
from django.http import JsonResponse


return JsonResponse(
    {
        "status": "ok",
    }
)

Redirect:

python

1
2
3
return redirect(
    "article-list",
)

File:

python

1
2
3
4
from django.http import FileResponse


return FileResponse(file)

Not found:

python

1
2
3
4
from django.http import Http404


raise Http404

HTTP Status Codes

Responses include a status code.

Common examples include:

Status Meaning
200 Successful response
201 Resource created
302 Redirect
400 Bad request
403 Forbidden
404 Not found
500 Server error

The status code tells the browser or API client how the request was handled.

Redirect Responses

A redirect does not directly display the final page.

Suppose a view returns:

python

1
2
3
4
return redirect(
    "article-detail",
    pk=article.pk,
)

The pipeline becomes:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
POST /articles/new/
   ↓
Django
   ↓
302 response
   ↓
Browser
   ↓
GET /articles/42/
   ↓
Django
   ↓
200 response

The redirect causes a completely new HTTP request.

This is important when understanding the Post/Redirect/Get pattern.

Exceptions in Views

Suppose a view raises an exception:

python

1
2
3
4
def article_detail(request, pk):
    raise ValueError(
        "Something went wrong."
    )

Django catches the exception higher in the request-handling system.

The result depends on:

  • exception type
  • middleware
  • DEBUG
  • configured error handlers

For example:

python

1
raise Http404

normally becomes a 404 response.

An unhandled programming error normally becomes a 500 response.

404 Handling

A common pattern is:

python

1
2
3
4
article = get_object_or_404(
    Article,
    pk=pk,
)

If the object exists:

text

1
Continue view

If it does not:

text

1
2
3
404 handling
   ↓
404 response

With DEBUG=False, Django can render a custom 404.html template.

500 Handling

Unhandled server errors normally result in:

text

1
500 Internal Server Error

During development with:

python

1
DEBUG = True

Django displays a detailed technical error page.

In production, detailed debug pages should not be exposed to users.

Response Middleware

After the view returns a response, that response travels back outward through middleware.

For example:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
View
   ↓
MessageMiddleware
   ↓
AuthenticationMiddleware
   ↓
CommonMiddleware
   ↓
SecurityMiddleware
   ↓
Server

Middleware may modify:

  • headers
  • cookies
  • status codes
  • response bodies

before the response leaves Django.

Security Headers

Security middleware may add or enforce HTTP security behavior.

For example, production responses may include headers related to:

  • HTTPS
  • content-type protection
  • referrer behavior
  • other browser security mechanisms

This happens after or around normal view processing rather than being repeated inside every individual view.

Cookies

A response can set cookies:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
response = HttpResponse(
    "Preferences saved."
)

response.set_cookie(
    "theme",
    "dark",
)

return response

The browser receives the cookie in the response.

On later requests, the browser may send it back:

python

1
2
3
theme = request.COOKIES.get(
    "theme"
)

The flow is:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Django response
   ↓
Set-Cookie
   ↓
Browser stores cookie
   ↓
Later request
   ↓
Cookie header
   ↓
request.COOKIES

Sessions Across Requests

Sessions build on the request-response cycle.

A view may write:

python

1
request.session["cart_count"] = 3

Django associates that session data with the browser.

On a future request:

python

1
2
3
4
count = request.session.get(
    "cart_count",
    0,
)

The important point is that HTTP requests themselves are independent.

Sessions provide application-level continuity between those separate requests.

Messages Across Redirects

Django’s messages framework can preserve short messages across a redirect.

Example:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from django.contrib import messages


messages.success(
    request,
    "Article created.",
)

return redirect(
    "article-list",
)

On the following request, a template can display:

django

1
2
3
{% for message in messages %}
    <p>{{ message }}</p>
{% endfor %}

The flow is:

text

1
2
3
4
5
6
7
8
9
POST request
   ↓
Add message
   ↓
302 redirect
   ↓
New GET request
   ↓
Message displayed

Static Files Follow a Different Path

A template may contain:

django

1
2
3
4
<link
    rel="stylesheet"
    href="{% static 'css/site.css' %}"
>

The browser first receives the HTML.

It then makes another request:

text

1
GET /static/css/site.css

In production, that request is often handled by:

  • Nginx
  • a CDN
  • object storage
  • another static-file service

rather than by the normal Django view pipeline.

A page load may therefore involve many separate HTTP requests.

Media Files May Also Follow a Different Path

An uploaded image might appear as:

django

1
2
3
4
<img
    src="{{ profile.avatar.url }}"
    alt="Avatar"
>

The browser then sends another request for the media URL.

In production, media may be served by:

  • a web server
  • object storage
  • a CDN
  • a protected Django download view

Whether media passes through Django depends on the deployment and security requirements.

API Requests Use the Same General Pipeline

A JSON API request still enters through the same broad Django request system.

For example:

text

1
GET /api/articles/

may flow through:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Server
   ↓
Django
   ↓
Middleware
   ↓
URL resolver
   ↓
API view
   ↓
Database
   ↓
JSON response

The difference is mainly in how the view interprets the input and what response it returns.

Instead of HTML:

python

1
return render(...)

an API may return JSON:

python

1
return JsonResponse(...)

Django REST Framework adds additional layers such as:

  • parsers
  • authentication
  • permissions
  • serializers
  • content negotiation

but it still operates within Django’s broader request-response system.

Synchronous Requests

A traditional synchronous Django view looks like:

python

1
2
def article_list(request):
    ...

During its execution, work occurs in sequence.

Conceptually:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Receive request
   ↓
Run middleware
   ↓
Run view
   ↓
Query database
   ↓
Render template
   ↓
Return response

The request finishes before that execution context handles another piece of work.

Asynchronous Views

Django can also use asynchronous views:

python

1
2
async def status_view(request):
    ...

Async views are useful when a request spends time waiting on compatible asynchronous I/O operations.

The overall conceptual pipeline remains similar:

text

1
2
3
4
5
6
7
8
9
Request
   ↓
Middleware
   ↓
URL resolver
   ↓
Async view
   ↓
Response

However, synchronous and asynchronous components can interact differently depending on how the application is deployed.

Beginners should first understand the ordinary request-response model before optimizing around asynchronous execution.

Request Pipeline Example

Consider:

text

1
GET /articles/42/

The project contains:

python

1
2
3
4
5
6
urlpatterns = [
    path(
        "articles/",
        include("articles.urls"),
    ),
]

The application URLs contain:

python

1
2
3
4
5
6
7
urlpatterns = [
    path(
        "<int:pk>/",
        article_detail,
        name="article-detail",
    ),
]

The view contains:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def article_detail(request, pk):
    article = get_object_or_404(
        Article,
        pk=pk,
        is_published=True,
    )

    return render(
        request,
        "articles/article_detail.html",
        {
            "article": article,
        },
    )

The request pipeline is approximately:

text

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
1. Browser requests /articles/42/

2. Web server receives the request.

3. WSGI or ASGI passes it to Django.

4. Django creates an HttpRequest.

5. Request middleware runs.

6. Sessions are attached.

7. Authentication determines request.user.

8. URL resolver examines urlpatterns.

9. /articles/ matches include("articles.urls").

10. <int:pk>/ matches 42.

11. Django calls:
        article_detail(request, pk=42)

12. The view queries:
        Article.objects...

13. The database returns the article.

14. Django renders:
        article_detail.html

15. render() creates an HttpResponse.

16. Response middleware runs.

17. Django passes the response to the server.

18. Server sends the HTTP response.

19. Browser renders the HTML.

20. Browser requests CSS, JavaScript,
    images, and other referenced assets.

POST Request Example

Now consider creating an article.

Browser submits:

text

1
POST /articles/new/

The request pipeline may be:

text

 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
Browser
   ↓
POST request
   ↓
Middleware
   ↓
CSRF validation
   ↓
URL resolver
   ↓
article_create view
   ↓
ArticleForm(request.POST)
   ↓
form.is_valid()
   ↓
article.save()
   ↓
302 redirect
   ↓
Browser sends new GET request
   ↓
Article detail view
   ↓
200 HTML response

The redirect creates a second complete request pipeline.

Login Example

Suppose an anonymous user requests:

text

1
/dashboard/

The request may flow like:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Request
   ↓
SessionMiddleware
   ↓
AuthenticationMiddleware
   ↓
request.user = AnonymousUser
   ↓
URL resolver
   ↓
@login_required
   ↓
Not authenticated
   ↓
302 redirect to login

After login:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
POST /login/
   ↓
Credentials validated
   ↓
Session created
   ↓
Redirect
   ↓
GET /dashboard/
   ↓
SessionMiddleware
   ↓
AuthenticationMiddleware
   ↓
request.user = authenticated user
   ↓
Dashboard view

This demonstrates how sessions, middleware, authentication, and redirects all connect through the request pipeline.

Where Business Logic Should Live

The pipeline does not mean every operation belongs in the view.

A healthy separation might be:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Middleware
    Cross-cutting request behavior

URLs
    Route requests

Views
    Coordinate HTTP behavior

Forms
    Validate browser input

Models
    Represent persistent data

Services
    Perform application workflows

Templates
    Render HTML

For example:

python

 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
26
def checkout(request):
    form = CheckoutForm(
        request.POST or None,
    )

    if (
        request.method == "POST"
        and form.is_valid()
    ):
        order = create_order(
            user=request.user,
            data=form.cleaned_data,
        )

        return redirect(
            "order-detail",
            pk=order.pk,
        )

    return render(
        request,
        "checkout.html",
        {
            "form": form,
        },
    )

The view controls the HTTP flow.

The service handles the main business operation.

Request Pipeline and Database Transactions

Django does not automatically wrap every possible request in a database transaction unless the project is configured to do so.

Application code may use:

python

1
2
3
4
5
from django.db import transaction


with transaction.atomic():
    ...

A transaction controls database operations, not the entire HTTP request lifecycle.

For example:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Request
   ↓
View
   ↓
transaction.atomic()
   ↓
Database operations
   ↓
Commit
   ↓
Render response

External side effects such as:

  • sending email
  • deleting files
  • calling external APIs

do not automatically roll back when a database transaction fails.

Signals During a Request

Signals can also execute while the request is being processed.

For example:

python

1
article.save()

may trigger:

text

1
2
pre_save
post_save

The view does not necessarily call the signal receiver explicitly.

Conceptually:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
View
   ↓
article.save()
   ↓
pre_save
   ↓
Database write
   ↓
post_save
   ↓
Return to view

This is one reason excessive signal usage can make request behavior difficult to trace.

Logging the Pipeline

Logging is useful for understanding request flow.

For example:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import logging


logger = logging.getLogger(__name__)


def article_detail(request, pk):
    logger.info(
        "Loading article %s",
        pk,
    )

    ...

Middleware can also log requests globally.

Useful information may include:

  • method
  • path
  • status code
  • duration
  • user
  • request ID

Avoid logging:

  • passwords
  • session cookies
  • authentication tokens
  • sensitive personal data

Request IDs

Larger systems often assign each request a unique identifier.

Example:

text

1
request_id=64f289...

The same ID can be included in logs from:

text

1
2
3
4
5
Middleware
View
Service
Database-related logging
External API calls

This makes it easier to reconstruct what happened during one request.

Performance Through the Pipeline

A slow request may spend time in different places.

For example:

text

1
2
3
4
5
Middleware        5 ms
View logic        3 ms
Database        180 ms
Template         12 ms
External API    500 ms

Total:

text

1
approximately 700 ms

Understanding the pipeline helps locate where the delay actually occurs.

Common causes include:

  • too many database queries
  • slow external services
  • expensive middleware
  • large template workloads
  • file operations
  • repeated authentication or permission queries

Middleware Should Usually Stay Focused

Because middleware can affect every request, expensive middleware can become expensive for the entire application.

Good middleware candidates include:

  • request logging
  • global security behavior
  • authentication-related processing
  • request IDs
  • broad redirects

Poor middleware candidates often include:

  • feature-specific database workflows
  • unrelated business logic
  • large numbers of database queries
  • behavior needed by only one view

Use middleware for cross-cutting request concerns.

Common Beginner Mistakes

Thinking URLs Execute Business Logic

A URL pattern should normally map a path to a view.

It should not contain the application workflow itself.

text

1
2
3
URL
   ↓
View

not:

text

1
2
3
URL
   ↓
Database/business logic

Putting Everything in the View

Views can quickly become difficult to maintain when they contain:

  • validation
  • database operations
  • emails
  • payment logic
  • complex permissions
  • file processing
  • external APIs

Use forms, models, services, and other appropriate layers.

Reading request.POST Without Validation

Avoid:

python

1
email = request.POST["email"]

for significant user input.

Prefer:

python

1
2
3
4
5
6
form = ContactForm(
    request.POST,
)

if form.is_valid():
    email = form.cleaned_data["email"]

Confusing Query Parameters and POST Data

Query string:

text

1
/search/?q=django

uses:

python

1
request.GET

POST form:

html

1
<form method="post">

uses:

python

1
request.POST

Assuming Every Request Reaches a View

Middleware may return a response first.

CSRF validation may fail.

URL resolution may produce a 404.

An exception may occur before normal view completion.

Assuming a Redirect Continues the Same Request

A redirect ends the current request.

The browser then sends another request.

text

1
2
3
4
5
Request 1
    → 302

Request 2
    → GET destination

Confusing Static Asset Requests With the Main Page Request

Loading one page may generate many HTTP requests:

text

1
2
3
4
GET /articles/
GET /static/css/site.css
GET /static/js/site.js
GET /media/photo.jpg

These are separate requests.

Assuming request.user Appears Automatically

Authentication middleware is part of the configuration that provides normal request.user behavior.

Ignoring Middleware Order

Some middleware depends on earlier middleware.

For example, authentication normally relies on session functionality.

Running Expensive Work in Middleware

Middleware can execute for a very large portion of application traffic.

Keep it focused.

Returning Something Other Than a Response

A view must ultimately return a valid HTTP response.

Incorrect:

python

1
2
3
4
def article_list(request):
    return {
        "articles": [],
    }

Correct:

python

1
2
3
4
5
6
7
8
def article_list(request):
    return render(
        request,
        "articles/list.html",
        {
            "articles": [],
        },
    )

Debugging the Request Pipeline

When a request behaves unexpectedly, trace it layer by layer.

Start with:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
1. What URL did the browser request?
2. What HTTP method was used?
3. Did middleware change or reject it?
4. Which URL pattern matched?
5. Which view executed?
6. What request data reached the view?
7. Which database queries ran?
8. Did validation succeed?
9. What response was returned?
10. Did middleware modify the response?

The browser’s network tools can reveal:

  • request URL
  • method
  • status code
  • request headers
  • response headers
  • redirects
  • response body

Django logging can reveal the server-side path through the application.

A Minimal Request Pipeline

For a basic view:

python

1
2
3
4
5
def home(request):
    return render(
        request,
        "home.html",
    )

the simplified pipeline is:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Browser
   ↓
GET /
   ↓
Django server
   ↓
Middleware
   ↓
URL resolver
   ↓
home(request)
   ↓
render()
   ↓
Template engine
   ↓
HttpResponse
   ↓
Middleware
   ↓
Browser

A Database-Backed Request Pipeline

For:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def article_list(request):
    articles = Article.objects.filter(
        is_published=True,
    )

    return render(
        request,
        "articles/list.html",
        {
            "articles": articles,
        },
    )

the flow expands:

text

 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
Browser
   ↓
Request
   ↓
Middleware
   ↓
URL resolver
   ↓
View
   ↓
Django ORM
   ↓
Database
   ↓
QuerySet results
   ↓
Template
   ↓
HTML
   ↓
HttpResponse
   ↓
Middleware
   ↓
Browser

A Form Submission Pipeline

For a creation form:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Browser
   ↓
POST
   ↓
Middleware
   ↓
CSRF check
   ↓
URL resolver
   ↓
View
   ↓
Form validation
   ↓
Model save
   ↓
Database
   ↓
Redirect response
   ↓
Browser
   ↓
New GET request

This is one of the most common request patterns in a Django application.

When working on a Django feature, think about the request in this order:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
1. What request is arriving?

2. Which middleware affects it?

3. Which URL matches?

4. Which view handles it?

5. What authentication or permission
   checks are required?

6. What input needs validation?

7. What models or services are called?

8. What response should be returned?

9. What middleware affects the response?

10. What does the browser do next?

This mental model is often more useful than thinking about views, forms, models, and middleware as isolated Django topics.

Mini Reference

text

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
HttpRequest
    Django representation of an incoming request

request.method
    HTTP method such as GET or POST

request.GET
    Query-string data

request.POST
    Submitted form data

request.FILES
    Uploaded files

request.user
    Current authenticated or anonymous user

request.session
    Session data

Middleware
    Global request/response processing

URL resolver
    Matches a URL path to a view

View
    Coordinates request handling

ORM
    Communicates with the database

Template
    Produces rendered text such as HTML

HttpResponse
    Django representation of an outgoing response

redirect()
    Returns a redirect response

render()
    Renders a template into a response

Basic flow:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Request
   ↓
Middleware
   ↓
URL resolver
   ↓
View
   ↓
Response
   ↓
Middleware

Database flow:

text

1
2
3
4
5
View
   ↓
ORM
   ↓
Database

Template flow:

text

1
2
3
4
5
6
7
8
9
View
   ↓
Context
   ↓
Template
   ↓
HTML
   ↓
HttpResponse

Django’s request pipeline describes the path an HTTP request takes through the application before a response is returned.

The main ideas are:

  • a browser sends an HTTP request
  • a web or application server passes the request to Django
  • Django creates an HttpRequest
  • middleware can inspect or modify the request
  • the URL resolver selects a view
  • the view coordinates application behavior
  • forms can validate incoming data
  • models and the ORM communicate with the database
  • templates can render HTML
  • the view returns an HttpResponse
  • middleware can modify the outgoing response
  • the server sends the response back to the browser
  • redirects start a new request
  • static and media files may follow separate request paths
  • authentication, sessions, CSRF, and messages all participate in the broader request-response cycle

Once this pipeline is clear, many Django features stop looking like separate pieces.

URLs decide where the request goes. Middleware controls what happens around the request. Views decide what the application should do. Models handle persistent data. Forms handle input validation. Templates determine how HTML is rendered. Responses determine what is sent back to the client.

Together, these pieces form the request-response cycle at the center of every Django application.

Join the Newsletter

Practical insights on Django, backend systems, deployment, architecture, and real-world development — delivered without noise.

Get updates when new guides, learning paths, cheat sheets, and field notes are published.

No spam. Unsubscribe anytime.



There is no third-party involved so don't worry - we won't share your details with anyone.