The Prisma Performance Playbook: 3 Common Mistakes Killing Your App's Speed
Learn how to identify and resolve the three most common Prisma performance pitfalls that are silently destroying your application's speed.
Prisma is arguably the most important ORM in the modern JavaScript Ecosystem. It is intuitive, type-safe and highly scalable.
I use it daily, either for my E-Commerce platform Entro or for my various client and personal projects. It allows me to ship production apps quickly by abstracting away the complex SQL glue code.
But as with any powerful tool, that convenience comes with a trade-off.
Prisma makes it dangerously easy to write inefficient database queries. Because the abstraction layer is so smooth, developers often forget that a single line of JavaScript can trigger massive, resource-intensive SQL operations. I have seen production codebases where fetching a blog post title inadvertently loaded the entire database table into memory.
This is highly inefficient. As developers, we need to think about the resource cost of our code. Bad performance doesn't just frustrate users, it inflates cloud bills and wastes infrastructure.
This article breaks down the three most common Prisma performance mistakes I have encountered, and exactly how to fix them.
Understanding the Cost
Before we optimize, we must understand what is happening under the hood. Prisma is not magic, it is a SQL generator.
Consider the seemingly harmless snippet:
1await db.user.findMany({
2 include: { posts: true }
3})This snippet fetches every user and every post associated with them. That seems manageable if you have 10 users. But what if you have 100,000 users? And each has 10 posts? You are asking your database to return over a million rows of data.
Let's look at the raw SQL query this generates:
1--- Query 1: Fetch all User columns
2SELECT "id", "email", "name", "createdAt", ... FROM "User"
3
4--- Query 2: Fetch all Post columns for those IDs
5SELECT "id", "title", "content", "published", "authorId"
6FROM "Post"
7WHERE "authorId" IN ($1, $2, ... $100000)In this example, even if we assume a user only has four columns, the payload is massive. The cost isn't just in the database execution, it's in the network transfer and the Node.js memory parsing. Such an unoptimized query can result in seconds of loading time.
The key takeaway is that seemingly small snippets of Prisma code can have a massive impact on your backend.
Mistake 1: The "Select All" Trap (Over-fetching)
The include keyword is one of the most commonly abused features in Prisma. By default, it acts like a SELECT *, fetching every column from the joined table.
If you only need to display the titles of a user's posts, why fetch the content, publishedAt, and tags columns? This is called Over-Fetching.
The Solution: Be specific. Instead of fetching everything, tell Prisma exactly what you need using select.
1// Bad: Fetches every column
2const users = await db.user.findMany({
3include: { posts: true }
4});
5
6// Good: Fetches only the ID and Title
7const users = await db.user.findMany({
8select: {
9 id: true,
10 posts: {
11 select: { title: true } // Precision fetching
12 }
13}
14});In this code, we use the select keyword to act as a filter. Prisma translates this into a much leaner SQL query. This simple change can reduce your query payload size by 90% or more.
Mistake 2: The N+1 Problem
Have you ever run a query inside a loop, or inside a .map() function? If so, you have likely created the N+1 Problem.
Let's look at this example:
1// 1. Fetch all users (1 Query)
2const users = await db.user.findMany();
3
4for (const user of users) {
5// 2. Fetch posts for EACH user (N Queries)
6const posts = await db.post.findMany({
7 where: { userId: user.id }
8})
9}What exactly is happening here? First, we fetch all users (1 query). Then, the for loop runs. If there are 1,000 users, the code inside the loop runs 1,000 times. This results in 1,001 total requests to your database. This will bring even a powerful server to a crawl.
The Solution: Let Prisma handle the relation.
You might notice that the solution to this problem looks familiar:
1const users = await db.user.findMany({
2select: {
3 id: true,
4 posts: {
5 select: { title: true }
6 }
7}
8});You might ask: "Stefan, isn't this the same code as Mistake 1?" Yes, it is. And that is the beauty of Prisma.
Prisma is smart enough to handle relation mapping automatically. Instead of firing a query for every user, it uses a technique called Dataloader under the hood to fetch all the required data in just two optimized queries.
By using select (or include) correctly, you solve both Over-Fetching and the N+1 problem with one uniform solution.
Mistake 3: The "Await" Waterfall
This mistake often happens in Server Components or API routes where you need data from multiple unrelated sources, perhaps a User, some Statistics, and a Product.
You might write your queries like this:
1// Bad: Serial Execution
2const user = await db.user.findUnique(...); // Waits 200ms
3const stats = await db.stats.findMany(...); // Waits 200ms
4const product = await db.product.findUnique(...); // Waits 200msIf you write your code like this, you are creating an "Await" Waterfall. The second query cannot start until the first one finishes. If each request takes 200ms, your total wait time is 600ms.
The Solution: Parallelize the requests.
Since the stats query doesn't depend on the user query, they should run at the same time.
1// Good: Parallel Execution
2const [user, stats, product] = await Promise.all([
3db.user.findUnique(...),
4db.stats.findMany(...),
5db.product.findUnique(...),
6]);By wrapping the promises in Promise.all(), we trigger all three requests simultaneously. In our scenario, the total time drops from 600ms to just 200ms (the time of the slowest query).
This is a 66% improvement in performance simply by changing how we structure our JavaScript.
Closing remarks
There are many ways to optimize a Backend, but these three are the most common issues I see when auditing code.
Writing queries with Prisma is incredibly easy, but just because it is easy doesn't mean it is efficient. As Full-Stack developers, it is our responsibility to look beyond the abstraction and ensure our applications respect both the user's time and our infrastructure's resources.
Implementing these three patterns will save you from huge resource bills, headaches, and slow loading times.
Thanks for reading.
Happy Coding!
Stefan