skip to content

WSGI & ASGI Servers

The application objects in wsgi.py and asgi.py, run by a WSGI or ASGI server behind a reverse proxy. Interviewers ask why runserver is not a production server and how many workers to run.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

5

Why is Django's runserver command not meant for production, and what serves a Django project there instead?

level: juniorimportance: must knowfreq 70%

answer

  1. built for the edit-reload loop
  2. never audited or load-tested
  3. a separate application server
  4. module path plus :application

basics

~20 s

runserver is a development server: it auto-reloads, runs in one process and was never security-audited or performance-tested. Production runs the project's wsgi.py or asgi.py application object under a real WSGI or ASGI server, usually behind a reverse proxy.

solid answer

~40 s

`manage.py runserver` exists for the edit-reload loop: it restarts on code changes, runs system checks, and with `django.contrib.staticfiles` serves static files while `DEBUG` is true. Django's docs state it has not been through security audits or performance tests, and since Django 5.2 it prints a warning saying so at start-up. It is a single process that starts a thread per request, with nothing to manage or recycle workers. In production you point a WSGI server (Django's docs cover Gunicorn, uWSGI, mod_wsgi and Granian) at `myproject.wsgi:application`, or an ASGI server (Daphne, Hypercorn, Uvicorn, Granian) at `myproject.asgi:application`, and put a reverse proxy in front for TLS, slow clients and static files.

code

bash · 8 lines
bash
# development
python manage.py runserver

# production: a WSGI server loads the application object from wsgi.py
gunicorn myproject.wsgi:application

# or an ASGI server loads it from asgi.py
uvicorn myproject.asgi:application

go deeper

for a junior

Know that runserver is only for development and that production runs a WSGI or ASGI server pointed at the project's application object.

for a middle

Explain what runserver does for you (reloader, checks, static files under DEBUG), which setting it reads, and how a production server is given myproject.wsgi:application.

for a senior

Describe the production stack of reverse proxy, application server and Django, which job each layer owns, and the settings that must change alongside the server.

for a principal

Choose the serving stack for a team: WSGI or ASGI server, proxy responsibilities and a standard configuration every Django service shares.

## What runserver is for `python manage.py runserver` (or `django-admin runserver`) is Django's **development server**. It is built around a developer's edit-and-refresh loop: - the **auto-reloader** restarts the server when Python code changes; - the **system check framework** runs at start-up and after each reload; - it is **multithreaded by default**, creating a new thread for each request (`--nothreading` turns that off); - with `django.contrib.staticfiles` installed, it serves static files — but only while `DEBUG = True`, or with the `--insecure` flag; - it binds to `127.0.0.1:8000` by default, reachable only from the same machine. It loads the WSGI application named by the **`WSGI_APPLICATION`** setting, which `startproject` sets to `<project>.wsgi.application`. It never uses `asgi.py`. ## Why it is not a production server The docs are blunt: *do not use this server in a production setting*. Since **Django 5.2** it also prints a warning at start-up saying it is a development server (suppressed only by setting the `DJANGO_RUNSERVER_HIDE_WARNING` environment variable to `"true"`). The reasons: 1. **No security audit and no performance testing.** Making it production-grade is explicitly outside Django's scope. 2. **One process.** There is no pool of worker processes to spread load across CPU cores, replace a crashed or bloated worker, or restart gracefully during a deploy. 3. **Static files the slow way.** The staticfiles handler that `runserver` uses is described in the docs as grossly inefficient and probably insecure, and it switches off when `DEBUG` is false. 4. **Development features cost resources.** The reloader watches the code tree, and a thread per request with no upper limit is not a capacity model. ## What production looks like | Layer | Job | Django's part | |---|---|---| | Reverse proxy | TLS, buffering slow clients, serving static and media files, routing | none; Django only needs to trust what it forwards | | Application server | manages worker processes or event loops, speaks WSGI or ASGI | loads `application` from `wsgi.py` or `asgi.py` | | Django application object | turns each request into a response through middleware, URLconf and views | built once per process by `get_wsgi_application()` or `get_asgi_application()` | The application server is given a dotted path such as `myproject.wsgi:application` or `myproject.asgi:application`. Django's deployment docs include getting-started guides for Gunicorn, uWSGI, mod_wsgi and Granian on the WSGI side, and Daphne, Hypercorn, Uvicorn and Granian on the ASGI side; which one you pick is an operations choice, not a Django one. ## Settings that change with the server Moving off `runserver` goes together with production settings: `DEBUG = False`, a real `ALLOWED_HOSTS`, HTTPS-related settings, and static files collected and served by something other than the development handler. Those have their own checklist (`manage.py check --deploy`), but a candidate should at least mention that switching servers without switching settings is only half the job. ## What interviewers listen for - the plain statement that `runserver` is for development only, with a reason beyond "it's slow"; - that production uses a separate WSGI or ASGI server pointed at the project's `application` object; - that a reverse proxy normally sits in front; - bonus: static files stop being served by `runserver` once `DEBUG` is off, which is why pages lose their CSS the first time people try it.

  • A page loses all its CSS as soon as DEBUG is set to False under runserver. Why?
    The staticfiles version of `runserver` serves static files only while `DEBUG` is true, or when started with `--insecure`. With `DEBUG = False` it stops, so static URLs return 404. In production static files are collected with `collectstatic` and served by the proxy or a dedicated static-files layer, not by the development handler.
  • Which file does runserver load, and which one does an ASGI server load?
    `runserver` loads the WSGI callable named by the `WSGI_APPLICATION` setting, by default `application` in `<project>/wsgi.py`, and never touches `asgi.py`. An ASGI server is given `<project>.asgi:application` directly. Both files build the same Django project; they differ in the protocol the server speaks.

saying these in an interview costs you the question

  • runserver is fine in production if you pass --noreload
  • Binding runserver to 0.0.0.0 makes it production-ready
  • runserver keeps serving static files after DEBUG is set to False
  • runserver uses asgi.py when the project has async views
  • The only problem with runserver is that it is single-threaded
open as a page

What do a Django project's wsgi.py and asgi.py contain, and how does DJANGO_SETTINGS_MODULE decide which settings they load?

level: middleimportance: must knowfreq 55%

basics

~10 s

Each file sets a default DJANGO_SETTINGS_MODULE with os.environ.setdefault and builds a module-level application via get_wsgi_application() or get_asgi_application(). A value already in the environment wins, so each deployment picks its settings without editing the file.

open as a page

When sizing worker processes and threads for a Django deployment, how does Django's one-connection-per-thread model limit the numbers?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Django opens a separate database connection in every thread that queries, so each worker thread in each process and instance can hold one. The database's connection limit therefore caps processes times threads, and persistent connections keep idle workers' connections open too.

open as a page

A Django project has mostly sync views plus a few async views that call slow external APIs; should you serve it with WSGI or ASGI?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Under WSGI an async view runs in its own one-off event loop while holding a worker, so awaits inside one view overlap but requests do not. ASGI pays off when async-capable middleware reaches views that stream or wait long on upstream I/O.

open as a page

When a reverse proxy serves a Django project under /shop/, how does Django learn that prefix so reverse() and redirects include it?

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

Django takes a script prefix per request from the WSGI SCRIPT_NAME or the ASGI root_path, or from FORCE_SCRIPT_NAME when set, and reverse() prepends it. The server must pass the prefix that way rather than baking /shop/ into the URLconf.

open as a page