The N+1 query is the most common performance bug in early-stage web apps, and it hides until it doesn't. You loop over a queryset in a template or a serializer, touch a related field on each row, and every touch fires its own database query. Ten rows feels instant in development; ten thousand rows in production turns a list view into a five-second stall. Django 6.1 ships a fix that works even when you forgot to plan for it — and a mode that turns the bug into a test failure. Here's the whole thing, with the exact API.

The problem, in two lines#

This is the shape of almost every N+1 you'll ever write:

books = Book.objects.all()          # 1 query
for book in books:
    print(book.author.name)         # +1 query PER book → N more

That's 1 + N queries. The classic fix is prefetch_related("author") — but only if you remembered to add it on this exact queryset, in this exact code path. Miss one and the regression ships silently.

The fix: .fetch_mode(models.FETCH_PEERS)#

Django 6.1 adds a QuerySet method, .fetch_mode(), that takes one of three constants from django.db.models. The one that kills N+1 is FETCH_PEERS:

from django.db import models

books = Book.objects.fetch_mode(models.FETCH_PEERS)   # 1 query
for book in books:
    print(book.author.name)                            # +1 query TOTAL, not per book

When a deferred or related field is missing on an instance, FETCH_PEERS fetches it for every instance that came from the same QuerySet at once — the release notes call it "an on-demand prefetch_related()." Your loop of N follow-up queries becomes a single peer fetch. Total cost: two queries, no matter how many rows.

The difference from prefetch_related() is when the decision happens. prefetch_related() demands you declare the access pattern before you run the query. FETCH_PEERS reacts to the access as it happens, so it fixes the N+1 you didn't predict — which is the only kind that ever hurts you in production.

The three modes#

There are three fetch modes, all in django.db.models:

Use RAISE to make N+1 a test failure#

FETCH_PEERS fixes the query storm; RAISE proves a path never had one. Apply it to a hot queryset and any unplanned lazy load throws instead of quietly hitting the database:

from django.core.exceptions import FieldFetchBlocked
from django.db import models

# A performance-critical endpoint that must not issue surprise queries.
books = Book.objects.select_related("author").fetch_mode(models.RAISE)
for book in books:
    print(book.author.name)      # fine — author was select_related()'d
    print(book.publisher.name)   # raises FieldFetchBlocked — publisher wasn't

Wire that into a test around your slowest endpoint and an accidental future .field access — the kind a teammate adds without thinking — fails CI instead of shipping a stall. That's the real unlock: N+1 stops being an invisible regression and becomes a visible, catchable one.

What to do before the August GA#

Django 6.1 is at release candidate (6.1rc1, published July 22, 2026), with the final expected in August; it supports Python 3.12–3.14. Don't wait for GA to plan:

  1. Test the RC against your two or three slowest list endpoints.
  2. Set FETCH_PEERS on the querysets those views iterate — measure the query count drop with Django Debug Toolbar or assertNumQueries.
  3. Add a RAISE test around each hot path so the fix can't silently regress.

Fetch modes don't retire select_related/prefetch_related — those are still right when you know your access pattern up front. They're the safety net for the cases you didn't. This release was one of six developer-tool launches worth acting on the week of July 26 — the rest are in The Founder's Toolchain, Week of July 26.