How I Caught and Fixed an N+1 Query in My Django REST API The article describes how the author identified and fixed an N+1 query problem in a Django REST API, where lazy ORM evaluation caused excessive database queries when serializing related fields like ForeignKey and ManyToMany relationships. The fix involved adding `select_related()` for ForeignKey relations and `prefetch_related()` for ManyToMany relations to the queryset, reducing the query count from 61 to just 3 for a 30-post list endpoint. The author emphasizes that developers should proactively check for N+1 queries during code review whenever nested serializers or related field accesses are present. Every performant API eventually runs into the same silent killer: the N+1 query problem. It doesn't crash your app. It doesn't throw errors. It just quietly makes every list endpoint slower as your data grows — and it's almost invisible until Sentry flags it in production. Today, Sentry caught one on my /api/blog-posts/ endpoint. Here's exactly what happened and how I fixed it in three lines of code. What Is an N+1 Query? An N+1 query happens when your code fetches a list of N records, then fires an additional query per record to fetch related data — totalling 1 + N database hits instead of a flat 2 or 3. In Django, this usually happens silently because the ORM is lazy by default. Accessing a related object on a model instance that wasn't eagerly loaded triggers a fresh SELECT on the spot. With 30 blog posts, that's 30 silent queries you never wrote. The Offending Code The BlogPostViewSet looked clean on the surface: class BlogPostViewSet viewsets.ReadOnlyModelViewSet : queryset = BlogPost.objects.all serializer class = BlogPostSerializer lookup field = "uid" And the serializer: class BlogPostSerializer serializers.ModelSerializer : tags = BlogTagSerializer many=True, read only=True series = BlogSeriesSerializer read only=True ... Spot the problem? BlogPost has two relations: - series — a ForeignKey to BlogSeries - tags — a ManyToManyField to BlogTag When DRF serializes a list of 30 posts, it accesses post.series and post.tags on each one. Without eager loading, Django fires two extra queries per post — one to fetch the series, one to fetch the tags. That's 1 + 60 queries for a 30-post list. The featured action had the same issue: python @action detail=False, methods= "get" def featured self, request : queryset = BlogPost.objects.filter date published isnull=False .order by "-date published", :3 A fresh BlogPost.objects call with no eager loading. The Fix Django gives me two tools for this: - — for select related ForeignKey and OneToOne relations. Issues a SQL JOIN and fetches everything in a single query. - — for prefetch related ManyToMany and reverse FK relations. Issues a second query and caches the results in Python. The fix: class BlogPostViewSet viewsets.ReadOnlyModelViewSet : queryset = BlogPost.objects.select related "series" .prefetch related "tags" serializer class = BlogPostSerializer lookup field = "uid" @action detail=False, methods= "get" def featured self, request : queryset = BlogPost.objects.select related "series" .prefetch related "tags" .filter date published isnull=False .order by "-date published" :3 serializer = self.get serializer queryset, many=True return Response serializer.data With 30 posts, the list endpoint now costs 3 queries regardless of dataset size: SELECT FROM core blogpost ... SELECT FROM core blogseries WHERE id IN ... SELECT FROM core blogtag INNER JOIN core blogpost tags WHERE blogpost id IN ... The Bonus Fix While auditing the blog endpoint, I spotted the same pattern in TestimonialViewSet . Its serializer accesses project.title and project.slug , but the queryset had no select related : Before queryset = Testimonial.objects.all After queryset = Testimonial.objects.select related "project" One extra line, one less N+1. How to Spot This in Your Own Code The pattern is always the same — look for any ViewSet or view where: - The queryset has no select related or prefetch related - The serializer accesses a related field source="relation.field" , nested serializers, SerializerMethodField that touches obj.relation Tools that help catch this before Sentry does: - — shows query counts per request in the browser django-debug-toolbar https://github.com/jazzband/django-debug-toolbar - — raises exceptions in tests when N+1 queries are detected nplusone https://github.com/jmcarp/nplusone - Sentry Performance — catches it in production with query traces The best time to catch an N+1 is during code review. Any time you write a nested serializer, ask: does the queryset for this view eagerly load this relation? Takeaway The Django ORM's lazy evaluation is a feature, not a bug — but it requires discipline at the queryset layer. A clean-looking viewset with objects.all is often hiding a query storm one serializer away. The rule of thumb: every relation accessed in a serializer needs a corresponding select related or prefetch related on the queryset. Make it a checklist item on every PR that touches a ViewSet.