Measure first
Run a baseline benchmark with autocannon -c 50 -d 30 http://localhost:3000/your-endpoint. Write down the number. Every optimization should be validated against this baseline. If your change doesn't move the needle, move on.
1. Fix N+1 queries (2–20x improvement)
The single most common API performance killer. Fetching a list of N items and then making a separate database query for each item.
// ✗ N+1: 1 query for projects + N queries for owner
const projects = await prisma.project.findMany();
const withOwners = await Promise.all(
projects.map(p => prisma.user.findUnique({ where: { id: p.userId } }))
);
// ✓ Single query with JOIN
const projects = await prisma.project.findMany({
include: { owner: { select: { id: true, name: true, email: true } } },
});2. Add database indexes (2–100x on indexed queries)
-- Find queries doing sequential scans
SELECT * FROM pg_stat_user_tables WHERE seq_scan > 100;
-- Add the missing index
CREATE INDEX CONCURRENTLY idx_projects_user_id ON projects(user_id);
CREATE INDEX CONCURRENTLY idx_projects_status_created ON projects(status, created_at DESC);3. Redis caching (5–50x for cache hits)
Cache the result of expensive queries that don't change on every request. Even a 30-second TTL on a complex aggregation query can eliminate most of the load.
4. Compression (network 60–80% reduction)
import compression from 'compression';
app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) return false;
return compression.filter(req, res);
},
threshold: 1024, // Only compress responses > 1KB
}));5. Connection pooling (3–10x under concurrent load)
Opening a new database connection per request is expensive (10–100ms per connection). A pool reuses connections. Most apps should never open a connection per request.
6. Select only the columns you need
// ✗ Fetches all columns including large text/JSONB fields
const users = await prisma.user.findMany();
// ✓ Only fetch what the response needs
const users = await prisma.user.findMany({
select: { id: true, name: true, email: true, createdAt: true },
});7. Cluster mode (1x CPU cores improvement)
import cluster from 'cluster';
import { cpus } from 'os';
if (cluster.isPrimary) {
cpus().forEach(() => cluster.fork());
cluster.on('exit', () => cluster.fork()); // auto-restart
} else {
startServer();
}8. HTTP keep-alive for outbound requests
import { Agent } from 'https';
const httpsAgent = new Agent({ keepAlive: true, maxSockets: 50 });
// Use with node-fetch / undici
fetch(url, { agent: httpsAgent });9. Move slow operations off the hot path
Email sending, PDF generation, image processing, third-party webhook calls — these don't need to complete before you respond to the user. Enqueue them and return 202 Accepted.
10. Switch to Fastify
For pure framework overhead, Fastify is 4–6x faster than Express. Combined with fast-json-stringify for schema-based serialization, Fastify is measurably faster for high-throughput APIs. The migration from Express is lower friction than most teams expect.