Skip to content

feat(settings): time out opening a database connection - #215

Merged
bryangingechen merged 2 commits into
masterfrom
feat/db-connect-timeout
Oct 11, 2026
Merged

bryangingechen merged 2 commits into
masterfrom
feat/db-connect-timeout

Conversation

@bryangingechen

@bryangingechen bryangingechen commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Opening a database connection had no time limit of its own, so psycopg used its built-in default of 130 s. Against a Postgres that accepts the TCP connection and then never answers, a request waited past gunicorn's 120 s worker timeout and ended with the worker killed rather than an error. The health endpoint's notes recorded this gap. Celery tasks waited the same way.

What changes

  • DJANGO_DB_CONNECT_TIMEOUT, wired through base.py and .env.example, sets libpq's connect_timeout in DATABASES["default"]["OPTIONS"]. It applies to each address the host name resolves to.
    • Default 10 s.
    • Accepts 2 to 600. psycopg turns anything below 2 into 2, and 0 or less into its own 130 s, so those values refuse to start instead of quietly meaning something else.
    • Blank or unset means the default, as an env file copied from .env.example may have it.
    • It is always passed, so libpq's PGCONNECT_TIMEOUT no longer has any effect. Nothing in the repo used it.
  • Docs: docs/production_image.md (Configuration, and the /api/health bullet: against a database that doesn't answer, the 503 comes after about the timeout, so a probe with a shorter timeout times out instead, and fails either way). The health endpoint's notes in qb_site/api/AGENTS.md and health.py now name the setting instead of describing the gap.

User-visible effects

None while the database answers. When it hangs, requests and tasks fail after about 10 s instead of 130 s. No migrations.

Testing

  • core.tests.test_db_connect_timeout (5 tests):
    • default 10 for unset, empty and blank;
    • set values are used;
    • 0, 1, -5, 601, ten and 2.5 refuse to start, with the setting's name in the error;
    • the active settings carry the value in OPTIONS, and Django's own connection parameters, built from them, pass it to psycopg;
    • against a listening socket that never answers, Django's own connection code, with the configured OPTIONS and a 2 s timeout, raises OperationalError after about 2 s, where it would otherwise wait for psycopg's 130 s.
  • uv run ruff check ., uv run ruff format --check .
  • Review (a separate agent) checked that Django 5.2.6 and 6.1.2 both pass OPTIONS through, and that psycopg 3.2.10 and 3.3.6 both apply the timeout per attempt, with the same 2 s floor and 130 s fallback. The review fixes are in 8eede328. Left for a follow-up: a server that freezes after connecting still holds a request until gunicorn's timeout. TCP keepalives or tcp_user_timeout would cover that.
  • bash scripts/repo_check_compose.sh: all 13 steps passed locally (core 119, syncer 543, analyzer 606, api 28, zulip_bot 350, console 96, site_analytics 81 tests). Compose's .env from .env.example sets the new value, and every service connects with it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QoXLf4KSfqzMFrdZGQG9Up

bryangingechen and others added 2 commits October 11, 2026 17:31
Opening a database connection had no time limit of its own, so psycopg
used its default of 130 s. Against a Postgres that accepts the
connection and never answers, a request then outlasted gunicorn's
120 s worker timeout, and ended with the worker killed instead of an
error.

DJANGO_DB_CONNECT_TIMEOUT sets libpq's connect_timeout in the database
OPTIONS, applied to each address the host resolves to: 10 s by default,
2 to 600 accepted. psycopg turns anything below 2 into 2, and 0 or less
into its own 130 s, so those values refuse to start rather than mean
something unexpected. Blank means the default, as an env file copied
from .env.example may have it.

Tested against a listening socket that never answers, through Django's
own connection code: the connection fails after about 2 s. Documented in
.env.example, docs/production_image.md, and the health endpoint's notes,
which said opening the connection had no limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoXLf4KSfqzMFrdZGQG9Up
- The behavioural test now also checks that Django's own connection
  parameters, built from the unmodified settings, carry the configured
  timeout, so it covers the wiring in base.py as well as psycopg keeping
  to it.
- Docs: against a silent database, /api/health answers 503 after about
  the timeout per address, so a probe with a shorter timeout times out
  instead; either way it fails.
- base.py and .env.example say that the setting, always passed, makes
  libpq's PGCONNECT_TIMEOUT ineffective.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoXLf4KSfqzMFrdZGQG9Up
@bryangingechen
bryangingechen marked this pull request as ready for review October 11, 2026 22:10
@bryangingechen
bryangingechen merged commit 5cbf9b0 into master Oct 11, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant