ADR — CONN_MAX_AGE=0 con Daphne ASGI
Contexto
CreaRack Pro usa Daphne como servidor ASGI. En config/settings/base.py, "daphne" primera entrada de INSTALLED_APPS (requerimiento Django Channels para habilitar HTTP2/WebSocket desde arranque). La app mantiene WebSocket persistentes para Sentinel de red y terminales interactivas; bajo carga moderada puede haber decenas de hilos Daphne activos simultáneamente.
CONN_MAX_AGE no definido en base.py; se delega a entornos (production.py, development.py). En 4 ocasiones distintas un valor CONN_MAX_AGE > 0 llegó a producción, provocando saturación PostgreSQL y pérdida de disponibilidad.
Problema
Daphne gestiona cada conexión HTTP/WS en un hilo propio. Con CONN_MAX_AGE > 0, Django mantiene la conexión BD abierta y ociosa durante esa ventana. El Sentinel de red genera ráfagas de reconexiones en eventos; en picos, 200+ conexiones idle se acumulan simultáneamente y agotan max_connections de PostgreSQL. Resultado: OperationalError: too many connections, app inaccesible.
Patrón repetido en: 327af62, revert c1ede18→4c9d37b, revert bc2e1d8→a6f5392, más un cuarto incidente no consolidado. Valor >0 siempre introducido como efecto colateral de operaciones pgbouncer, no intencional.
Opciones consideradas
CONN_MAX_AGE=60 — reutiliza conexiones 60s. Mejora latencia media pero incompatible con modelo de hilos Daphne; produce el problema descrito.
CONN_MAX_AGE=0 — Django abre/cierra conexión en cada request. Overhead <1ms en red Docker interna. Elimina riesgo de acumulación. Sin infraestructura adicional.
Connection pooling externo (PgBouncer transaction mode) — delega pooling fuera de Django; compatible con ASGI porque pool vive en proceso independiente. Combina con CONN_MAX_AGE=0: Django no reutiliza, PgBouncer recicla del pool transparentemente. Opción adoptada a largo plazo.
Decisión
CONN_MAX_AGE=0 en todos los entornos con Daphne activo, combinado con PgBouncer en transaction mode como capa de pooling externo. Django nunca mantiene conexiones persistentes; PgBouncer gestiona pool independientemente del número de hilos ASGI.
Regla operativa: si se añade, elimina o reconfigura PgBouncer, CONN_MAX_AGE=0 debe preservarse explícitamente. Los dos valores no son interdependientes y no deben tocarse en el mismo commit sin revisión deliberada.
Consecuencias
- Positivo: PostgreSQL nunca supera umbral de idle connections; Sentinel reconecta sin riesgo.
- Positivo: Config predecible y auditable; sin estado de conexión implícito por hilo.
- Negativo: Cada request paga coste de apertura (~0.3-1ms en Docker local); aceptable para carga actual.
- Negativo: Requiere PgBouncer correctamente configurado en
transaction mode; ensession modepooling no compatible con ASGI.
Status
Accepted. Revertido incorrectamente 4 veces antes de consolidar. Cualquier PR que modifique CONN_MAX_AGE en producción debe referenciar este ADR y justificar desviación.
Véase también
- [[concept—saas—multi-tenancy]]
- [[decision—20260101—django-ninja-vs-drf]] — Decisión del framework REST sobre el que corre Daphne