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

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.

All posts