Django 5.1: PostgreSQL Connection Pools
django python
Django 5.1 was released in August 2024. The headliner: native PostgreSQL connection pooling. Here’s everything new and how to use it.
Connection Pooling
The Problem
# Without pooling:
# Every request → New database connection
# Connection setup: ~50-100ms overhead
# High-traffic app = thousands of connections
# PostgreSQL: max_connections limit hit
Traditional solution: Add PgBouncer as middleware.
Django 5.1 Solution
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydb',
'USER': 'user',
'PASSWORD': 'password',
'HOST': 'localhost',
'PORT': '5432',
'OPTIONS': {
'pool': {
'min_size': 2,
'max_size': 10,
}
}
}
}
Native pooling, no external service needed.
How It Works
# Uses psycopg3's built-in connection pool
# Based on psycopg_pool library
Request 1 → Get connection from pool →
Use → Return to pool
Request 2 → Reuse same connection →
Use → Return to pool
# No connection setup overhead for each request
Requirements
# Requires psycopg3 (not psycopg2)
pip install "psycopg[binary,pool]"
# Django automatically uses psycopg3 if available
ENGINE = 'django.db.backends.postgresql'
# Will use psycopg3 by default in Django 5.1
Configuration Options
'OPTIONS': {
'pool': {
'min_size': 2, # Minimum connections
'max_size': 10, # Maximum connections
'timeout': 30, # Wait timeout (seconds)
'max_lifetime': 3600, # Max connection age
'max_idle': 300, # Max idle time
}
}
When to Still Use PgBouncer
- Transaction-level pooling (for serverless)
- Multiple applications sharing one pool
- Advanced connection management
For most Django apps, native pooling is sufficient.
LoginRequiredMiddleware
Before
# Decorating every view
from django.contrib.auth.decorators import login_required
@login_required
def dashboard(request):
...
@login_required
def settings(request):
...
# Easy to forget one
Django 5.1
# settings.py
MIDDLEWARE = [
# ...
'django.contrib.auth.middleware.LoginRequiredMiddleware',
]
LOGIN_URL = '/accounts/login/'
All views require login by default.
Exempting Views
from django.contrib.auth.decorators import login_not_required
@login_not_required
def public_page(request):
return render(request, 'public.html')
# For class-based views
from django.utils.decorators import method_decorator
@method_decorator(login_not_required, name='dispatch')
class PublicView(View):
...
Improved Admin Interface
Dark Mode Preference
# Automatically respects system dark mode preference
# No configuration needed
# Or set explicitly
ADMIN_FORCE_COLOR_SCHEME = 'dark' # or 'light'
Admin History Improvements
# Now shows which fields changed
# Before: "Changed user"
# After: "Changed email, is_active on user"
Model Fields
GeneratedField Improvements
from django.db.models import F, GeneratedField
from django.db.models.functions import Concat
class Person(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
# Now supports more expressions
full_name = GeneratedField(
expression=Concat(F('first_name'), ' ', F('last_name')),
output_field=CharField(max_length=201),
db_persist=True
)
CompositePrimaryKey (Preview)
# Still experimental, but available
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.IntegerField()
class Meta:
# Composite primary key
pk = ['order', 'product'] # Preview syntax
Full support expected in Django 5.2.
Template Improvements
querystring Tag
<!-- Before -->
<a href="?page={{ page }}&{% for k, v in request.GET.items %}{% if k != 'page' %}{{ k }}={{ v }}&{% endif %}{% endfor %}">
<!-- After -->
<a href="{% querystring page=page %}">Next</a>
<!-- Add/replace parameters -->
{% querystring page=1 sort="name" %}
<!-- ?existing_param=value&page=1&sort=name -->
<!-- Remove parameter -->
{% querystring page=None %}
Form Improvements
BoundField.as_field_group()
# Render complete field with label, widget, errors
{{ form.email.as_field_group }}
# Equivalent to (but shorter):
<div>
{{ form.email.label_tag }}
{{ form.email }}
{{ form.email.errors }}
</div>
Performance
Query Optimization
# Improved prefetch_related
queryset = Author.objects.prefetch_related(
Prefetch(
'books',
queryset=Book.objects.filter(published=True),
to_attr='published_books'
)
)
# Better memory usage
Migration
From Django 5.0
# Check for deprecation warnings
python -W error::DeprecationWarning manage.py check
# Run migrations
python manage.py migrate
# Key changes:
# - psycopg3 is now preferred
# - Some deprecated features removed
Checklist
[ ] Upgrade to Python 3.10+ (if not already)
[ ] Install psycopg3: pip install "psycopg[binary,pool]"
[ ] Test database connections
[ ] Add LoginRequiredMiddleware if desired
[ ] Test existing views still work
[ ] Run full test suite
Configuration Example
# Full Django 5.1 database config with pooling
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME', 'myapp'),
'USER': os.environ.get('DB_USER', 'postgres'),
'PASSWORD': os.environ.get('DB_PASSWORD', ''),
'HOST': os.environ.get('DB_HOST', 'localhost'),
'PORT': os.environ.get('DB_PORT', '5432'),
'CONN_MAX_AGE': None, # Let pool manage
'CONN_HEALTH_CHECKS': True,
'OPTIONS': {
'pool': {
'min_size': 2,
'max_size': 10,
}
}
}
}
Final Thoughts
Django 5.1 is a focused release. Native connection pooling simplifies deployment architecture. LoginRequiredMiddleware reduces security boilerplate. The querystring tag is a small but welcome quality-of-life improvement.
Worth upgrading, especially if you’re using PostgreSQL.
Simpler deployment, better defaults.