Opened 3 weeks ago

Last modified 7 days ago

#36964 assigned Cleanup/optimization

Clarify how persistent connections interact with runserver

Reported by: Adam Sołtysik Owned by: Youssef Tarek Ali
Component: Documentation Version: 5.2
Severity: Normal Keywords:
Cc: Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

From https://docs.djangoproject.com/en/5.2/ref/databases/#caveats:

The development server creates a new thread for each request it handles, negating the effect of persistent connections. Don’t enable them during development.

Something here seems to be incorrect. I'm using 'CONN_MAX_AGE': None with the development server, and it works as expected. My requests are significantly faster than with the default setting. When analyzing with SELECT * FROM pg_stat_activity WHERE datname = '<dbname>', I can see a single connection created and persisting after the first request, and it disappears after autoreload.

Change History (12)

comment:1 by Mankameshwar Mishra, 3 weeks ago

I’d like to work on a documentation patch for this issue.

comment:2 by Shubh Rai, 3 weeks ago

Sequential requests reuse the same thread, which results in single DB connection visible . The connection is dropped only on autoreload.

It seems the documentation warning is for concurrent requests, where multiple threads may create separate connections, rather than sequential development testing. I believe that the documentation is not wrong but little bit clarification can be given.

comment:3 by Youssef Tarek Ali, 3 weeks ago

Has patch: set
Owner: set to Youssef Tarek Ali
Status: newassigned

I have assigned this to myself and drafted a documentation update. The new wording clarifies that persistent connections do work for sequential requests in the development server, while noting that they are reset upon auto reload and that concurrent requests still trigger new threads. Verified the build locally with make html.

PR: https://github.com/django/django/pull/20825

comment:4 by Shubh Rai, 3 weeks ago

Owner: changed from Youssef Tarek Ali to Shubh Rai

comment:6 by Shubh Rai, 3 weeks ago

Triage Stage: UnreviewedAccepted

A pull request has been opened for this documentation clarification:

https://github.com/django/django/pull/20827

The PR updates the wording about persistent connections when using the development server to clarify that they may still be observed when CONN_MAX_AGE is enabled, but their behavior may not reflect production environments due to autoreload and the thread-per-request model.

comment:7 by VIZZARD-X, 3 weeks ago

Triage Stage: AcceptedUnreviewed

Reverting the triage stage back to 'Unreviewed' for now so it can be officially assessed and accepted by a core reviewer/triager, as per the standard workflow.

comment:8 by Natalia Bidart, 3 weeks ago

Owner: changed from Shubh Rai to Youssef Tarek Ali

comment:9 by Shubh Rai, 2 weeks ago

,Sorry for taking ownership of this ticket. I'm still new to the Django ticket workflow and didn't realize it was already being worked on. My apologies for the confusion.

comment:10 by Tim Graham, 9 days ago

Has patch: unset
Summary: Documentation incorrectly states that persistent connections don't work with runserverClarify how persistent connections interact with runserver
Triage Stage: UnreviewedAccepted

The quoted statement in the documentation was written by one of Django's most esteemed contributors (2ee21d9f0d9eaed0494f3b9cd4b5bc9beffffae5). While something may have changed in the intervening years, we need a more rigorous explanation.

I believe that multiple threads may be used by the built-in runserver, even if requests are not concurrent. I verified this making this modification:

diff --git a/django/db/backends/base/base.py b/django/db/backends/base/base.py
index 23015a57a3..94e7197dde 100644
--- a/django/db/backends/base/base.py
+++ b/django/db/backends/base/base.py
@@ -51,6 +51,9 @@ class BaseDatabaseWrapper:
     queries_limit = 9000
 
     def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS):
+        print("DB Wrapper")
+        import threading
+        print(threading.get_ident())
         # Connection related attributes.
         # The underlying database connection.
         self.connection = None

And making several requests:

Starting WSGI development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

DB Wrapper
139299259958976
[14/Mar/2026 00:41:02] "GET /admin/ HTTP/1.1" 200 11255
[14/Mar/2026 00:41:04] "GET /admin/polls/choice/ HTTP/1.1" 200 11343
[14/Mar/2026 00:41:04] "GET /admin/jsi18n/ HTTP/1.1" 200 3342
[14/Mar/2026 00:41:08] "GET /admin/polls/question/ HTTP/1.1" 200 13477
DB Wrapper
139299250517696
[14/Mar/2026 00:41:09] "GET /admin/jsi18n/ HTTP/1.1" 200 3342
[14/Mar/2026 00:41:12] "GET /admin/polls/choice/ HTTP/1.1" 200 1134

I don't believe the AI-generated patch correctly explained the nuances here.

in reply to:  10 comment:11 by Youssef Tarek Ali, 9 days ago

Has patch: set

Replying to Tim Graham:

The quoted statement in the documentation was written by one of Django's most esteemed contributors (2ee21d9f0d9eaed0494f3b9cd4b5bc9beffffae5). While something may have changed in the intervening years, we need a more rigorous explanation.

I believe that multiple threads may be used by the built-in runserver, even if requests are not concurrent. I verified this making this modification:

diff --git a/django/db/backends/base/base.py b/django/db/backends/base/base.py
index 23015a57a3..94e7197dde 100644
--- a/django/db/backends/base/base.py
+++ b/django/db/backends/base/base.py
@@ -51,6 +51,9 @@ class BaseDatabaseWrapper:
     queries_limit = 9000
 
     def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS):
+        print("DB Wrapper")
+        import threading
+        print(threading.get_ident())
         # Connection related attributes.
         # The underlying database connection.
         self.connection = None

And making several requests:

Starting WSGI development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

DB Wrapper
139299259958976
[14/Mar/2026 00:41:02] "GET /admin/ HTTP/1.1" 200 11255
[14/Mar/2026 00:41:04] "GET /admin/polls/choice/ HTTP/1.1" 200 11343
[14/Mar/2026 00:41:04] "GET /admin/jsi18n/ HTTP/1.1" 200 3342
[14/Mar/2026 00:41:08] "GET /admin/polls/question/ HTTP/1.1" 200 13477
DB Wrapper
139299250517696
[14/Mar/2026 00:41:09] "GET /admin/jsi18n/ HTTP/1.1" 200 3342
[14/Mar/2026 00:41:12] "GET /admin/polls/choice/ HTTP/1.1" 200 1134

I don't believe the AI-generated patch correctly explained the nuances here.

Thanks for the logs and feedback, Natalia. I see now that my previous rationale was off.

I wrote this update myself to make sure the technical details are correct. The new PR explains that database connections are thread-local and that there's no guarantee of thread reuse for sequential requests, even with Keep-Alive. I also added a note about the auto-reloader.

The documentation builds correctly locally. Let me know if I've still missed anything.

New PR: https://github.com/django/django/pull/20905

Version 0, edited 9 days ago by Youssef Tarek Ali (next)

comment:12 by Tim Graham, 9 days ago

How is the development server's behavior different from multithreaded WSGI servers used in production? Do they "guarantee that sequential requests will use the same thread"?

comment:13 by Adam Sołtysik, 9 days ago

The quoted statement in the documentation was written by one of Django's most esteemed contributors (2ee21d9f0d9eaed0494f3b9cd4b5bc9beffffae5). While something may have changed in the intervening years, we need a more rigorous explanation.

The statement was probably correct when it was written, and what changed later was the keep-alive support added around https://github.com/django/django/pull/10609.

How is the development server's behavior different from multithreaded WSGI servers used in production? Do they "guarantee that sequential requests will use the same thread"?

It seems that, unlike production servers, runserver generally keeps creating new threads for each client, as mentioned earlier and confirmed in a forum post. But the threads are reused with HTTP keep-alive, which allows persistent DB connections to work.

Note: See TracTickets for help on using tickets.
Back to Top