Eliminate N+1 queries in AdonisJS with Lucid eager loading
Use AdonisJS Lucid preload to fetch related records in one extra query instead of one per row.
10 Jun 2026, 19:24 UTC

The N+1 problem hides in a simple loop
Your AdonisJS endpoint returns a list of users, and your code loops through each user to display their posts. With three users locally, the page feels instant. In production with fifty users, response time climbs and database CPU spikes. The cause is usually not missing indexes. It is one query to fetch users, followed by one hidden query for every user’s posts.
This pattern is called an N+1 query problem: one initial query plus N additional queries, where N is the number of rows returned. Lucid is AdonisJS’s Active Record-style ORM, meaning model instances carry row data and expose relationship helpers that can trigger database access when you touch them.
Fix it with preload
Call preload on the model query before fetching rows. Lucid issues a second query for all related posts and attaches the results to each user instance. Accessing user.posts afterward causes no further database queries.
import { HttpContext } from '@adonisjs/core/http'
import User from '#models/user'
export default class UsersController {
async index({ response }: HttpContext) {
const users = await User.query().preload('posts')
for (const user of users) {
// No extra query here; posts are already attached
response.ok({ user: user.serialize(), posts: user.posts })
}
}
}
Two queries run regardless of whether you return five users or five hundred: one for users and one for posts.
Filter and nest preloaded relationships
You can scope the preload with a callback, which is useful when you only need published posts:
const users = await User.query().preload('posts', (query) => {
query.where('is_published', true).orderBy('created_at', 'desc')
})
Nested relationships use dot notation. This loads users, their posts, and the author of each post:
const users = await User.query().preload('posts.author')
Load a relationship conditionally with load
If you already fetched a model and only need the relationship under certain conditions, use the instance method load:
const user = await User.findBy('email', '[contact removed]')
if (includePosts) {
await user.load('posts')
}
Calling load inside a loop recreates the N+1 problem, so reserve it for single instances or genuinely conditional branches.
Trade-offs and version differences
Preloading every relationship increases payload size and memory usage even when the client does not need the data. Preload also does not automatically select only needed columns. Combine it with select when working with wide tables:
const users = await User.query()
.select('id', 'name', 'email')
.preload('posts', (query) => query.select('id', 'user_id', 'title'))
Relationship names must match the Lucid model definition. AdonisJS v6 favors decorators such as @hasMany, while v5 used static get relationships(). Confirm your major version before copying model syntax.
Verify before you deploy
Run this in your AdonisJS project root to confirm the major version:
node ace --version
Where to run: terminal in the project root. Permissions: none. Expected check: output shows 5.x or 6.x. Risk: none; this is read-only.
Next, add a temporary route, enable Lucid SQL logging, and request the endpoint. Confirm the adapter emits exactly two queries: one for users and one for posts, independent of user count. Remove the temporary route and logging after verification; leaving debug logging enabled in production can slow the application and expose query details.
Exact logging configuration varies by AdonisJS version, so check the Lucid model chapter for your release before enabling it.
Closing
Use preload when you know a relationship is needed for every row in a result set. Use load for single, conditional cases. Never call either inside a loop without checking the query log. The practical check is simple: two queries should appear no matter how many users you return.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.