WebTech1223411 logo WebTech1223411Web tech, read closely
Back-End

How N+1 Queries Slow an API and How to Spot Them

An endpoint that returns 20 blog posts with their authors takes 40 milliseconds on your laptop. In production, with 500 posts per page requested by a mobile app, it takes 2.8 seconds and the database CPU graph looks like a mountain range.

A request trace waterfall showing a long column of identical short database queries

An endpoint that returns 20 blog posts with their authors takes 40 milliseconds on your laptop. In production, with 500 posts per page requested by a mobile app, it takes 2.8 seconds and the database CPU graph looks like a mountain range. Nothing in the code looks wrong. There is no slow query in the log, just thousands of fast ones.

That pattern is the N+1 query problem, and it is behind a large share of slow APIs I have been asked to profile. N+1 queries are easy to write, invisible in small test data and painful at scale. The good news is that they are also among the easiest performance problems to find and fix once you know what to look for.

What N+1 Queries Actually Are

The name describes the count. Your code runs one query to fetch a list of N records, then one more query for each record to fetch something related. Fetch 20 orders, then loop over them and load each order's customer, and you have run 21 queries. Fetch 500, and you have run 501.

Each of those queries is fast on its own, often under a millisecond inside the database. The cost is everything around it: a network round trip to the database, connection checkout from the pool, query parsing, result serialisation and object creation in your application. At 1 to 3 milliseconds of overhead per round trip, 500 extra queries adds one to one and a half seconds before any real work happens.

Why ORMs Make N+1 Queries So Easy to Write

Most N+1 problems come from object relational mappers doing exactly what they were designed to do. Lazy loading means a related object is fetched only when your code touches it. Write order.customer.name inside a loop, and the ORM quietly runs a query on every pass.

This is common in Django, Rails ActiveRecord, Laravel Eloquent, Hibernate, Sequelize and Prisma alike. Serialisers make it worse, because a JSON serialiser that includes a nested field walks every relationship for every row, and the query lives in a file nobody thinks of as database code.

GraphQL multiplies it

GraphQL resolvers run per field, per object. A query asking for posts, each post's author and each author's latest three comments can turn one request into hundreds of queries unless the server batches lookups.

How to Spot N+1 Queries Before Users Do

You will not see N+1 problems in a slow query log, because no single query is slow. You need to count queries per request instead.

  • Development query logging. Turn on SQL logging locally and watch for the same statement repeated with a different id each time.
  • Debug toolbars. Django Debug Toolbar, Laravel Debugbar and similar tools show a query count per page and highlight duplicates.
  • Detection libraries. The Bullet gem for Rails and nplusone for Django flag lazy loads in a loop and can fail tests when one appears.
  • APM traces. In production, tracing tools show a single request as a waterfall. A long column of identical short database spans is the signature.

The most useful habit is a query count assertion in tests. Write a test that calls the endpoint with ten records and with fifty, and assert the query count is the same. If it grows with the data, you have an N+1.

Three Ways to Fix Them

Every fix comes down to fetching related data in bulk instead of one row at a time.

ApproachHow it worksBest for
Join in one queryFetch parents and children with a SQL joinOne to one or small one to many relations
Eager load with a second queryFetch all children with WHERE id IN (list)Larger one to many relations
Batch loaderCollect ids during a request, load them togetherGraphQL and deeply nested data

In ORMs these have familiar names: select_related and prefetch_related in Django, includes in Rails, with in Laravel, include in Prisma and Sequelize, JOIN FETCH in Hibernate. For GraphQL, the DataLoader pattern collects every author id requested during one tick and runs a single query for all of them.

The result is usually dramatic. That 501 query endpoint becomes 2 queries, and response time drops from seconds to tens of milliseconds.

The Advice to Always Eager Load Everything

Once a team gets burned by N+1 queries, someone suggests a rule: always eager load every relation, everywhere. That swaps one problem for another.

Eager loading a relation you do not use still costs a query, or a heavier join, and the memory to hold the results. A join across three one to many relations can multiply row counts, so 100 posts with 10 comments and 5 tags each return 5,000 rows to build 100 objects. I have seen endpoints get slower after a blanket eager loading change, not faster.

Load what the response actually uses. That is one more reason to design responses deliberately, with explicit fields and optional includes, as described in how to design a REST API other developers enjoy using. When the shape of a response is fixed, its queries are predictable.

Keep N+1 Queries From Coming Back

N+1 problems return whenever someone adds a field to a serialiser or a template. A fix that relies on everyone remembering will not last. A few guards that do:

  1. Enable strict lazy loading in development where the ORM supports it. Laravel's preventLazyLoading and Rails strict_loading raise an error on any lazy load.
  2. Add query count assertions to tests for every list endpoint.
  3. Set an alert on queries per request in your APM tool, for example anything above 50.
  4. Review serialiser and resolver changes as database changes in code review.

Fewer queries per request also means each client costs your database less, which matters when you protect busy endpoints with limits, as covered in how rate limiting protects an API without punishing real users.

FAQ

Is a database cache a fix for N+1 queries?

It hides the symptom. Caching may cut database load, but you still pay the round trip to the cache for every row, and cold caches bring the problem straight back. Fix the queries first.

Are N+1 queries ever acceptable?

For lists that are always tiny, such as five settings rows on an admin page, the cost is negligible. The danger is lists that grow with users or content.

Do NoSQL databases have the same problem?

Yes. Fetching a list of documents and then one lookup per document is the same pattern. Embedding related data or using batch reads solves it.

Count Queries, Not Milliseconds

Pick your three busiest list endpoints this week. Turn on query logging, call each with a realistic page size and count the queries. If the count grows with the number of rows, eager load only what the response needs, add a query count test and turn on strict lazy loading in development. Most teams find at least one N+1 on the first try. More backend guides live in our backend section.

KO
Kofi Oosterhuis

Kofi spent years keeping APIs and game servers alive through traffic spikes and the occasional bad deploy. He covers back-end design, scaling and networking, with a preference for boring systems that do not page anyone at night.

More posts by Kofi

More in Back-End

01doorganico 084goad 3hot8 4465456 after5internprogram ahandfulofblessings ahmadpc ahomcybersecurity allodialresources annodex antiquesandperiodcollectibles aotc asianworldkashmir beedlectrics bloggertemplatefree bluefeatheragency bodyweightsolution campaign08blog carfixadvisor catstreetcollective cbstvsports cnfanspurchasing committeeof500yearsofdignityandresistance covertopsdjs cs-kaiyuangame cuestacomunicaciontotal cxoutlet dancebetgame deskgearhub destinyvillastay detectmagazine di33 diamondsteelbuildings donmo dyanadropship emarsysfinance emimattl escapechan eskitarih evautoplay everythingweddings-orlando farmzone feedviewerapp foxstudiosscratch gamevui123 gleasongrade greetingsday hacklabasu haskell iw-kaiyun jagoanproperti jilidskripsi joltbusinessfinance juegodepokergratis juegospucca kaeru-festival latestmehndidesigns lestanzediottaviano litemagazines localcommunityhero lustrsazan mariashebets menswheels newwebstechs1 nikkit ootheday oxigenvpn plurfitness prefetial probudgetcarpetcleaning psychicmrsmartin pyaten qvantor rejoignezlogis rexzbot riselayouts ruchin rwxqsgd5 sakaltime shibiaowang shop-bot skrtips strcoding tetapatrium topselller uneplanteartificielle vipertechcr wo-kygaming yamaha-mekar yeknava yoybuysheets zenunwa yamaha-mekar jilidskripsi ankertoto ankertoto ankertoto ankertoto