Solving the N+1 Query Problem in AdonisJS with Lucid Preloading
Stop killing your database with N+1 queries. Learn how to use Lucid's preload mechanism in AdonisJS to fetch relationships efficiently and keep your application fast.
06 Sept 2026, 08:08 UTC

The Hidden Performance Killer: The N+1 Problem
You have a dashboard that lists 20 recent orders, and for each order, you need to display the customer's name. If you fetch the orders and then call a method to get the customer inside a loop, you aren't making one query—you're making 21. One for the list of orders, and 20 individual queries for each customer.
This is the classic N+1 query problem. In a development environment with ten rows of data, it feels instantaneous. In production with thousands of records, it creates a massive bottleneck that spikes database CPU and slows your response times to a crawl.
The solution in AdonisJS is Eager Loading via the Lucid ORM. Instead of fetching relationships lazily, you tell Lucid exactly which related data you need upfront, reducing those 21 queries down to just two.
Defining Relationships for Efficient Retrieval
Before you can optimize queries, your models must explicitly define how they relate. Lucid uses an Active Record pattern, meaning the model knows how to fetch its own dependencies.
For a standard Order-to-User relationship, you define a belongsTo relationship in the Order model. This tells Lucid that the orders table contains a foreign key (usually user_id) pointing to the users table.
The Preload Mechanism
The preload method is the primary tool for eager loading. When you call .preload('relationshipName'), Lucid executes a separate query to fetch all related records for the entire result set using an IN clause, then maps them back to the original models in memory.
Worked Example: Fetching Orders with Users and Products
Assume we are using AdonisJS v6. We want to fetch all orders, the user who placed them, and the products associated with those orders (a many-to-many relationship).
// In your Controller
import Order from '#models/order'
export default class OrdersController {
async index() {
const orders = await Order.query()
.preload('user') // Eager load the User (belongsTo)
.preload('products') // Eager load the Products (manyToMany)
.where('status', 'completed')
return orders
}
}
Verification and Diagnostics
To verify this is working, enable the SQL logger in your config/database.ts or via the environment. You should see exactly three queries regardless of how many orders are returned:
SELECT * FROM orders WHERE status = 'completed'SELECT * FROM users WHERE id IN (1, 2, 3...)SELECT * FROM products INNER JOIN order_product ... WHERE order_id IN (1, 2, 3...)
If you see a stream of SELECT * FROM users WHERE id = ? repeating dozens of times, you are still hitting the N+1 problem.
Managing Complexity and Trade-offs
While preloading is powerful, it is not a silver bullet. There are two primary risks to monitor as your application grows:
Memory Exhaustion
Preloading deeply nested relationships (e.g., .preload('user.profile.settings.preferences')) loads all that data into the application's RAM. If you are fetching 500 orders and each has a deep tree of relationships, you can quickly exhaust the Node.js heap memory.
The Bloated Model Trap
Because Lucid models encapsulate both data and database logic, there is a temptation to put complex business calculations inside the model. This leads to "Fat Models" that are hard to test. Keep your models focused on data definition and relationships; move complex business logic into separate Service classes.
Practical Implementation Summary
To ensure your AdonisJS application remains performant, follow these rules of thumb:
- Always preload any relationship you intend to use inside a
.map()orforEachloop. - Use constrained preloads if you only need a subset of related data (e.g.,
.preload('products', (query) => query.where('stock', '>', 0))). - Monitor SQL logs during development to catch unexpected query spikes before they reach production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.