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:
FETCH_ONE— the default. Fetches the missing field for the current instance only. This is today's lazy loading, and it's the behavior that produces N+1.FETCH_PEERS— fetches the missing field for all instances from the same QuerySet in one query. Your default choice for any loop.RAISE— doesn't query at all. Raisesdjango.core.exceptions.FieldFetchBlockedinstead.
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:
- Test the RC against your two or three slowest list endpoints.
- Set
FETCH_PEERSon the querysets those views iterate — measure the query count drop with Django Debug Toolbar orassertNumQueries. - Add a
RAISEtest 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.



