Database: every request and every worker poll opens a new database connection #1132
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#1132
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What exists today
config/settings/base.py:172-178buildsDATABASES["default"]fromPOSTULO_DATABASE_URLwith django-environ and sets onlyATOMIC_REQUESTS;CONN_MAX_AGEis left at Django's default of 0, so Django closes the connection at the end of every request (request_finishedrunsclose_old_connections).docker/Dockerfile:388-393runs 3 sync gunicorn workers; each request opens a connection, and on SQLite runsinit_command(PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;,config/sqlite.py:53), registers Django's own SQL functions and Postulo's twolikefunctions (core/sqlite_like.py:47-51, onconnection_created).core/management/commands/work.py:57-86builds adjango_tasks_dbWorkerevery--everyseconds (default 2.0). Upstreamdb_worker.py:108and:155callclose_old_connections()at the top and bottom of each pass, which withCONN_MAX_AGE = 0closes the connection, so the next pass reconnects: one connection every 2 s, about 43,200 a day, with nothing queued.send_due_reminders --loop --every 300(docker/compose.yml:63) keeps one connection for the process lifetime, since a management command fires no request signals; unaffected.Measurement (
work/r4_conn.pyin the audit scratchpad, Django 6.1.2's SQLite backend with Postulo's options andsqlite_likeinstalled, a file database on the local disk): opening, setting up and closing one connection took 2,746 microseconds on average over 300 cycles; oneSELECT 1on an open connection took 5.4 microseconds. Linux is likely faster at opening the file, but the setup is the same. PostgreSQL was not measured (no local server); a new PostgreSQL connection costs a TCP connect, SCRAM authentication and a new backend process each time.django-environ already accepts
?conn_max_age=60&conn_health_checks=trueonPOSTULO_DATABASE_URL(environ/environ.py:160-166), but nothing documents it.What would change
In
config/settings/base.py, besideATOMIC_REQUESTS:setdefaultkeeps whatever an operator put in the URL. Health checks make a connection dropped by a PostgreSQL restart cost one retry instead of a 500. With 3 web workers, one worker and one scheduler, a PostgreSQL instance holds at most 5 idle connections, far below the defaultmax_connectionsof 100. The test settings replaceDATABASESwholesale (config/settings/test.py:26-28) and are not affected. Document the variable and the URL options on the wiki page that lists the environment.No library needed; this is Django's own setting.
What it saves or protects
medium: about 2.7 ms of connection setup saved per request and per worker pass on SQLite (measured on Windows), and a TCP plus SCRAM handshake and a PostgreSQL backend fork saved on PostgreSQL; the worker alone opens about 43,000 connections a day
Accessibility
none
Decided by the maintainer (2026-10-31)
CONN_MAX_AGE 60 with health checks, its own environment variable, setdefault so an operator's value wins
Decisions
None(for ever) is also safe with health checks, but 60 bounds how long a connection outlives a configuration change.database_password.apply_password).--preload) beside this as one question to the user; they are separate changes and can be decided separately.Found by the efficiency and reliability audit of 2026-10-30 (finding R4; index postulo/postulo#1108). Checked against
mainatdaee30b15(2026-10-30).