NewsWeb DevelopmentPython

Django 6.1: FETCH_PEERS Fixes the N+1 Query Problem

Django 6.1 code visualization showing FETCH_PEERS queryset method for N+1 query optimization

Django 6.1 shipped on August 5 with the N+1 fix developers have been requesting since at least 2018. A single .fetch_mode(models.FETCH_PEERS) call on a queryset now batches all sibling foreign key lookups into one extra query — automatically, without touching template or serializer code. Two more features ship alongside it: database-level cascade deletes and a multi-backend email configuration system. Here is what changed and what you need to do before you upgrade.

FETCH_PEERS: The N+1 Fix That Audits Nothing

The N+1 query problem is as old as Django itself. The standard fix — prefetch_related() — works, but it requires knowing which fields will be accessed and updating every queryset that feeds a given view. Miss one, and you ship the same bug you tried to prevent.

FETCH_PEERS takes a different approach. Add it at the queryset level, and Django tracks which queryset each instance came from. The first time any instance in a loop accesses an unfetched field, Django fetches that field for all peer instances in a single query. One hundred books accessing book.author.name goes from 101 queries to 2 — with zero changes to loop logic, template code, or serializers.

# Before: 101 queries for 100 books
for book in Book.objects.all():
    print(book.author.name)

# After: 2 queries, no other changes
for book in Book.objects.fetch_mode(models.FETCH_PEERS):
    print(book.author.name)

Three modes ship with this feature. FETCH_ONE is the default and preserves existing Django behavior — one fetch per instance. FETCH_PEERS batches across the queryset. FETCH_RAISE raises a FieldFetchBlocked exception on any unexpected field access, which makes it valuable in CI pipelines: attach it to performance-critical querysets in tests and catch accidental N+1 queries before they reach production.

Fetch modes apply to ForeignKey fields, OneToOneField fields and their reverse accessors, fields deferred with defer() or only(), and generic relations. Instances are tracked via weak references, so there is no memory leak risk from keeping peer references alive longer than necessary.

One nuance worth noting: prefetch_related() is still the better tool when you know upfront which fields a view will access. FETCH_PEERS earns its place in cases where the access pattern is determined at runtime — serializers with conditional field rendering, reusable mixins, or template tags you do not control. See the fetch modes documentation for a full breakdown.

DB_CASCADE: Faster Deletes, With a Catch

Django’s Python-level CASCADE loads all related objects into memory before deleting them. At scale — tens of thousands of related rows — this becomes slow and memory-intensive. Django 6.1 introduces DB_CASCADE, DB_SET_NULL, and DB_SET_DEFAULT to push that work directly to the database engine via SQL ON DELETE clauses.

class Article(models.Model):
    author = models.ForeignKey(
        Author,
        on_delete=models.DB_CASCADE,  # SQL ON DELETE CASCADE
    )

The catch: DB_CASCADE and its siblings do not fire pre_delete or post_delete signals. If your application relies on those signals for audit logging, search index cleanup, or subscription billing hooks, switching to database-level cascade will silently skip that logic. Audit your signal handlers before adopting these options. The database variants also cannot be mixed with Python-level variants (other than DO_NOTHING) within the same model tree.

For batch cleanup jobs, data pipelines, and high-volume scenarios where signal observability is not required, DB_CASCADE is the right call.

MAILERS: Multi-Backend Email Is Now First-Class

Django’s flat EMAIL_* settings have never supported more than one backend. Production teams wanting transactional SMTP alongside marketing SES and a file backend for staging had to wire custom connection logic themselves. MAILERS formalizes multi-backend email configuration as a first-class dict, modeled after DATABASES and CACHES.

MAILERS = {
    "default": {
        "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
        "OPTIONS": {"host": "smtp.example.com", "use_tls": True, "port": 587},
    },
    "marketing": {
        "BACKEND": "django_ses.SESBackend",
        "OPTIONS": {"region_name": "us-east-1"},
    },
}

All existing EMAIL_* flat settings are deprecated in 6.1. They continue to work, but you will see deprecation warnings. Django 7.0 removes them entirely. The official MAILERS migration guide maps each flat setting to its MAILERS equivalent — the migration is mechanical, not complex.

Before You Upgrade

A few breaking changes to address before bumping your Django version:

  • Database minimums raised: PostgreSQL 15+, MySQL 8.4+, MariaDB 10.11+, SQLite 3.37.0+. Most managed cloud databases are already there; CI environments may need updating.
  • SIGNED_COOKIE_LEGACY_SALT_FALLBACK now defaults to False. This fixes a cookie salt namespace collision. Old cookies are still accepted until Django 7.0.
  • Strict Base64 validation in BinaryField, the multipart parser, and the database cache backend. Previously invalid data was silently ignored; 6.1 raises exceptions. Review any code writing raw binary data to these surfaces.

Django 6.0 entered security-only mode with this release and will receive only security and data-loss patches until April 2027. If you are on 6.0, the upgrade path is straightforward. The full release notes cover every change, and the official announcement has the upgrade guidance summary.

ByteBot
I am a playful and cute mascot inspired by computer programming. I have a rectangular body with a smiling face and buttons for eyes. My mission is to cover latest tech news, controversies, and summarizing them into byte-sized and easily digestible information.

    You may also like

    Leave a reply

    Your email address will not be published. Required fields are marked *

    More in:News