Difficulty: Intermediate
What is connection pooling and the N+1 query problem? How do you solve them in a Node.js application?
Connection pooling maintains a cache of database connections that are reused across requests instead of creating a new connection for each query. Creating a connection involves TCP handshake, authentication, and TLS negotiation - this overhead is significant at scale.
Prisma uses a built-in connection pool (default 5 connections for each Prisma Client instance). The pool manages acquiring, releasing, and recycling connections automatically. You can configure pool size via the connection string or Prisma config.
The N+1 problem occurs when you fetch a list of N items, then make N additional queries to fetch related data for each item. For example, fetching 100 users, then querying posts for each user individually results in 101 queries instead of 2.
Solutions include: eager loading (Prisma's include), batch loading (DataLoader pattern), joins (raw SQL), and selecting only needed fields. Prisma's include/select performs efficient JOINs or batched queries automatically.
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient({ log: ['query'] }); // Log queries
// BAD: N+1 problem - 1 query for users + N queries for posts
const users = await prisma.user.findMany();
for (const user of users) {
// Each iteration = 1 query!
const posts = await prisma.post.findMany({
where: { authorId: user.id }
});
user.posts = posts;
}
// Result: 101 queries for 100 users!
// GOOD: Eager loading with include - 2 queries total
const usersWithPosts = await prisma.user.findMany({
include: {
posts: {
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 5,
},
},
});
// Prisma runs: SELECT * FROM User; SELECT * FROM Post WHERE authorId IN (...);
// GOOD: Select only needed fields
const lightweight = await prisma.user.findMany({
select: {
id: true,
name: true,
_count: { select: { posts: true } },
},
});
include causes Prisma to batch-load related records in a single IN query. select limits the fields returned. Both solve N+1 efficiently.
// Via connection string
// postgresql://user:pass@host:5432/db?connection_limit=20&pool_timeout=10
// Via Prisma Client
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL + '?connection_limit=20'
}
}
});
// Singleton pattern - one client for the entire app
// db.js
let prisma;
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient();
} else {
// Prevent multiple instances in development (hot reload)
if (!global.__prisma) {
global.__prisma = new PrismaClient({ log: ['query'] });
}
prisma = global.__prisma;
}
module.exports = prisma;
// Monitor pool usage
prisma.$on('query', (e) => {
if (e.duration > 1000) {
console.warn(`Slow query (${e.duration}ms):`, e.query);
}
});
Use a singleton Prisma client to share the connection pool. In development, store on global to survive hot reloads without creating new pools.
const DataLoader = require('dataloader');
const prisma = require('./db');
// Batch function: receives array of keys, returns array of results
async function batchUsers(ids) {
const users = await prisma.user.findMany({
where: { id: { in: ids } },
});
// Must return results in same order as input ids
const userMap = new Map(users.map(u => [u.id, u]));
return ids.map(id => userMap.get(id) || null);
}
// Create loader (per request!)
const userLoader = new DataLoader(batchUsers);
// Usage: individual loads are batched automatically
const user1 = await userLoader.load(1); // Not executed yet
const user2 = await userLoader.load(2); // Not executed yet
// After current tick: single query WHERE id IN (1, 2)
// Results are cached within the request
const user1Again = await userLoader.load(1); // Returns cached
DataLoader collects individual load calls within a tick and batches them into a single query. Create a new loader per request to avoid cross-request caching.
Connection Pool, N+1 Query, DataLoader, Query Optimization