skip to content

In Django, why must the runserver development server never serve production traffic, and what serves the application instead?

level: juniorimportance: must knowfreq 68%

answer

  1. the docs' all-caps warning
  2. no security audit, no load testing
  3. built on the standard library's wsgiref
  4. autoreload and DEBUG-only static serving
  5. same application object, real server

basics

~10 s

runserver is a convenience server built on Python's wsgiref that Django never security-audited or performance-tested. Production serves the same WSGI or ASGI application object from wsgi.py or asgi.py through a dedicated application server.

solid answer

~40 s

`manage.py runserver` starts Django's lightweight development server, and the docs say in capitals not to use it in production: it has not gone through security audits or performance tests, and making it production-grade is explicitly outside Django's scope. It is built on the standard library's `wsgiref`, serves requests from a single process with a thread per request, restarts itself when Python files change, and, with `django.contrib.staticfiles` installed, serves static files only while `DEBUG=True` or with `--insecure`. Since Django 5.2 it also prints a startup warning saying exactly this. In production the application object that `wsgi.py` or `asgi.py` exposes is run by a real WSGI or ASGI server with several worker processes, `DEBUG=False` and static files served by a proper static-file layer.

code

bash · 4 lines
bash
python manage.py runserver                  # 127.0.0.1:8000, autoreload on
python manage.py runserver 0:8000           # all interfaces, still development only
python manage.py runserver --noreload --nothreading
DJANGO_RUNSERVER_HIDE_WARNING=true python manage.py runserver

go deeper

for a junior

Recall the docs' warning: runserver is not audited or performance-tested, and production uses a real WSGI or ASGI server running the application from wsgi.py or asgi.py.

for a middle

Explain what runserver actually does: wsgiref-based, threaded, autoreloading, DEBUG-gated static serving through staticfiles, system checks and the ALLOWED_HOSTS guard at startup.

for a senior

Show that you keep staging and production on the same serving stack, never patch runserver flags to make it look production-like, and treat DEBUG=True on a reachable host as an incident.

for a principal

Frame it as a scope decision: Django deliberately leaves serving to dedicated servers, so the team owns choosing, configuring and supervising that layer as part of the platform.

## What runserver is `runserver` is a built-in **management command**: you run it as `python manage.py runserver` (or `django-admin runserver` with settings configured). It starts a small HTTP server on `127.0.0.1:8000` by default and hands every request to the WSGI application named by the `WSGI_APPLICATION` setting — the same `application` object that `startproject` writes into `wsgi.py`. Under the hood it lives in `django/core/servers/basehttp.py` and subclasses the standard library's `wsgiref.simple_server`. A threaded variant is used by default, so each request gets its own thread inside one Python process. It exists so that a developer can save a file and immediately see the result in a browser. ## Why the docs forbid it in production The reference for the command carries an unusual all-caps warning: **do not use this server in a production setting**. The stated reasons are short and deliberate: - It **has not gone through security audits**. Nobody has reviewed its request parsing and connection handling against hostile traffic. - It **has not gone through performance tests**. There is no worker model, no tuned connection handling and no process supervision. - **Making it production-ready is outside Django's scope.** The project deliberately leaves serving to software built for it, so the gap will not be closed in a future release. On top of that, its developer conveniences are liabilities on a public host: - The **autoreloader** watches source files and restarts the process when they change. - With `django.contrib.staticfiles` installed, the command is replaced by the staticfiles version, which serves files from your apps' static directories — but only while `DEBUG=True` or with the `--insecure` flag. Running production with `DEBUG=True` to make that work also exposes Django's detailed error pages. - It is a **WSGI** server only; there is no built-in ASGI development server in core Django. Since **Django 5.2**, `runserver` prints a warning at startup that it is a development server unsuitable for production. Setting the environment variable `DJANGO_RUNSERVER_HIDE_WARNING` to `"true"` hides the message; it changes nothing else about the server. ## Development server vs production serving | Concern | `runserver` | Production setup | |---|---|---| | Purpose | Local feedback loop | Serving real users | | Security review | None, by design | Server built and hardened for it | | Processes | One process, a thread per request | Several worker processes, supervised | | Code reload | Automatic on file change | Only on deliberate deploy or restart | | Static files | Served while `DEBUG=True` or with `--insecure` | Collected once, served by a static-file layer | | Entry point | `WSGI_APPLICATION` setting | `application` in `wsgi.py` or `asgi.py` | ## What serves production instead The important point for an interview is that **the application object does not change**. `startproject` generates `wsgi.py` and `asgi.py`, each exposing a module-level `application`. A production WSGI or ASGI server imports that object and runs it with its own worker and connection model, typically behind a reverse proxy that terminates TLS. Choosing that server and tuning it belongs to deployment, not to the management command. ## Signs a project is misusing runserver Interviewers often turn the question around and ask how you would spot the mistake in someone else's project. The usual tells are: - A container image or process file whose start command is `python manage.py runserver 0.0.0.0:8000`. - `DEBUG=True` in the production environment, often added "because the CSS stopped loading" — the static files only worked through the development handler. - No `collectstatic` step anywhere in the build, for the same reason. - Code changes appearing in production without a deploy, because the autoreloader picked up an edited file. - Tracebacks with settings values visible to end users, which is `DEBUG=True` doing exactly what it is designed to do. Each of these is fixed the same way: serve the `application` object with a production server, run with `DEBUG=False`, and let the build collect static files. ## Useful options while developing 1. `runserver 0:8000` listens on all interfaces (`0` is a shortcut for `0.0.0.0`) so a phone on the same network can reach it — still a development server. 2. `--noreload` disables the autoreloader, useful under a debugger. 3. `--nothreading` serves requests one at a time, which can expose code that relies on concurrency. 4. `--ipv6` (or a bracketed address) binds to IPv6. 5. `--nostatic` stops the staticfiles version from serving static files. Two startup behaviours also teach something: `runserver` runs the system checks and warns about unapplied migrations before serving, and it refuses to start with `DEBUG=False` and an empty `ALLOWED_HOSTS` (it raises `CommandError: You must set settings.ALLOWED_HOSTS if DEBUG is False.`). Neither makes it production-grade; they simply stop obviously broken configurations from starting.

  • Why do static files work under runserver in development but return 404 once DEBUG is False?
    With `django.contrib.staticfiles` installed, its own `runserver` replaces the core one and wraps the app in a static handler, but only when `DEBUG=True` or `--insecure` is passed. With `DEBUG=False` the handler is off, because production is expected to collect static files once and serve them from a layer built for it, not from Django.
  • Is it acceptable to run runserver on 0.0.0.0 for a staging box that only testers reach?
    Binding to all interfaces only changes who can connect; the server is still unaudited, single-process and autoreloading, and staging usually runs with `DEBUG=True`, exposing detailed error pages. Staging should mirror production: the same application object under a real WSGI or ASGI server, so problems show up before launch.

runserver is a test kitchen at home: perfect for trying a recipe and tasting it instantly, but nobody inspected it for a restaurant licence, and the same recipe (the application object) is cooked for paying guests in a proper kitchen.

saying these in an interview costs you the question

  • runserver becomes production-ready once you pass --noreload
  • Setting DEBUG=False turns runserver into a hardened server
  • runserver is only slow because of the autoreloader
  • Running runserver inside a container makes it safe for production
  • Production needs a different application object than wsgi.py exposes