Node.js Performance: Clustering, Caching, and Profiling
이 글의 핵심
Practical Node.js performance tuning: cluster module and PM2, Redis and LRU caches, DB indexes and lean queries, async parallelism, memory and streams, gzip, and worker threads for CPU-heavy work.
Introduction
Why performance matters
Reasons to optimize:
- ✅ User experience: Lower latency and faster responses
- ✅ Cost: Better utilization of server resources
- ✅ Scale: Handle more concurrent users
- ✅ SEO: Page speed affects search rankings
Principles:
- Measure first: Do not guess—profile and measure
- Find bottlenecks: Improve the slowest parts first
- Iterate: Change one thing at a time
- Trade-offs: Balance performance vs readability and maintainability
1. Clustering
Cluster module
// cluster.js
const cluster = require('cluster');
const http = require('http');
const os = require('os');
const numCPUs = os.cpus().length;
if (cluster.isMaster) {
console.log(`Master process ${process.pid} running`);
// Fork worker processes
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// Restart workers on exit
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} exited`);
console.log('Starting new worker...');
cluster.fork();
});
} else {
// Workers run the HTTP server
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end(`Handled by worker ${process.pid}\n`);
});
server.listen(3000, () => {
console.log(`Worker ${process.pid} started`);
});
}
Run:
node cluster.js
# Master process 12345 running
# Worker 12346 started
# ...
PM2 cluster mode
# One process per CPU core
pm2 start app.js -i max
# Fixed count
pm2 start app.js -i 4
# Zero-downtime reload
pm2 reload app
2. Caching
In-memory cache
// Simple TTL cache
const cache = new Map();
function getCachedData(key, fetchFn, ttl = 60000) {
const cached = cache.get(key);
if (cached && Date.now() < cached.expiresAt) {
console.log('Cache hit');
return cached.data;
}
console.log('Cache miss');
const data = fetchFn();
cache.set(key, {
data,
expiresAt: Date.now() + ttl
});
return data;
}
// Usage
app.get('/api/users', async (req, res) => {
const users = getCachedData('users', async () => {
return await User.find();
}, 60000); // 1 minute
res.json({ users });
});
Redis caching
npm install redis
// cache.js
const redis = require('redis');
const client = redis.createClient({
host: 'localhost',
port: 6379
});
client.on('error', (err) => {
console.error('Redis error:', err);
});
client.connect();
async function setCache(key, value, ttl = 3600) {
await client.setEx(key, ttl, JSON.stringify(value));
}
async function getCache(key) {
const value = await client.get(key);
return value ? JSON.parse(value) : null;
}
async function deleteCache(key) {
await client.del(key);
}
module.exports = { setCache, getCache, deleteCache };
// Usage
const { getCache, setCache } = require('./cache');
app.get('/api/users/:id', async (req, res) => {
const { id } = req.params;
const cacheKey = `user:${id}`;
let user = await getCache(cacheKey);
if (user) {
console.log('Cache hit');
return res.json({ user, cached: true });
}
user = await User.findById(id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
await setCache(cacheKey, user, 3600);
res.json({ user, cached: false });
});
Cache invalidation
app.put('/api/users/:id', async (req, res) => {
const { id } = req.params;
const user = await User.findByIdAndUpdate(id, req.body, { new: true });
await deleteCache(`user:${id}`);
res.json({ user });
});
3. Database optimization
Indexes
// MongoDB
userSchema.index({ email: 1 });
userSchema.index({ name: 1, age: -1 });
userSchema.index({ email: 1 }, { unique: true });
const indexes = await User.collection.getIndexes();
console.log(indexes);
Query optimization
// Slow: N+1
const posts = await Post.find();
for (const post of posts) {
const author = await User.findById(post.author);
}
// Prefer populate
const posts = await Post.find().populate('author');
// Project only needed fields
const posts = await Post.find()
.select('title content author')
.populate('author', 'name email');
// lean(): plain objects, faster reads
const posts = await Post.find().lean();
Connection pool
mongoose.connect('mongodb://localhost:27017/mydb', {
maxPoolSize: 10,
minPoolSize: 2
});
const pool = new Pool({
max: 20,
min: 5,
idleTimeoutMillis: 30000
});
4. Async optimization
Parallelism
// Sequential (slow)
async function sequential() {
const users = await User.find();
const posts = await Post.find();
const comments = await Comment.find();
return { users, posts, comments };
}
// Total time: T1 + T2 + T3
// Parallel (fast)
async function parallel() {
const [users, posts, comments] = await Promise.all([
User.find(),
Post.find(),
Comment.find()
]);
return { users, posts, comments };
}
// Total time: max(T1, T2, T3)
Concurrency limit
const pLimit = require('p-limit');
async function processFiles(files) {
const limit = pLimit(5);
const results = await Promise.all(
files.map(file => limit(() => processFile(file)))
);
return results;
}
5. Memory management
Inspect memory
function logMemoryUsage() {
const used = process.memoryUsage();
console.log({
rss: `${Math.round(used.rss / 1024 / 1024)} MB`,
heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)} MB`,
heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)} MB`,
external: `${Math.round(used.external / 1024 / 1024)} MB`
});
}
setInterval(logMemoryUsage, 60000);
Avoid unbounded caches
// Bad: unbounded Map growth
const cache = new Map();
app.get('/api/data/:id', async (req, res) => {
const data = await fetchData(req.params.id);
cache.set(req.params.id, data);
res.json(data);
});
// Better: LRU
const LRU = require('lru-cache');
const cache = new LRU({
max: 500,
maxAge: 1000 * 60 * 60
});
app.get('/api/data/:id', async (req, res) => {
let data = cache.get(req.params.id);
if (!data) {
data = await fetchData(req.params.id);
cache.set(req.params.id, data);
}
res.json(data);
});
Streams for large payloads
// Bad: load entire file into memory
app.get('/download', async (req, res) => {
const data = await fs.promises.readFile('large-file.pdf');
res.send(data);
});
// Good: stream
app.get('/download', (req, res) => {
fs.createReadStream('large-file.pdf').pipe(res);
});
6. Profiling
Built-in CPU profiler
node --prof app.js
node --prof-process isolate-0x*.log > processed.txt
Chrome DevTools
node --inspect app.js
node --inspect-brk app.js
Open chrome://inspect and attach for CPU/memory profiling.
clinic.js
npm install -g clinic
clinic doctor -- node app.js
clinic bubbleprof -- node app.js
clinic heapprofiler -- node app.js
7. Benchmarking
Apache Bench
sudo apt install apache2-utils
ab -n 1000 -c 10 http://localhost:3000/
autocannon
npm install -g autocannon
autocannon -c 10 -d 10 http://localhost:3000/
Programmatic benchmark
const autocannon = require('autocannon');
async function runBenchmark() {
const result = await autocannon({
url: 'http://localhost:3000',
connections: 10,
duration: 10,
pipelining: 1
});
console.log('Req/sec:', result.requests.mean);
console.log('Latency ms:', result.latency.mean);
console.log('Throughput:', result.throughput.mean, 'bytes/sec');
}
runBenchmark();
8. Practical examples
Example 1: API response caching middleware
const express = require('express');
const redis = require('redis');
const app = express();
const client = redis.createClient();
await client.connect();
function cacheMiddleware(ttl = 3600) {
return async (req, res, next) => {
const key = `cache:${req.originalUrl}`;
try {
const cached = await client.get(key);
if (cached) {
console.log('Cache hit');
return res.json(JSON.parse(cached));
}
const originalJson = res.json.bind(res);
res.json = (data) => {
client.setEx(key, ttl, JSON.stringify(data));
return originalJson(data);
};
next();
} catch (err) {
next();
}
};
}
app.get('/api/users', cacheMiddleware(60), async (req, res) => {
const users = await User.find();
res.json({ users });
});
Example 2: Optimized posts query
async function getPostsWithAuthors() {
const posts = await Post.find();
for (const post of posts) {
post.author = await User.findById(post.author);
}
return posts;
}
async function getPostsWithAuthorsOptimized() {
return Post.find()
.populate('author', 'name email')
.select('title content author createdAt')
.lean()
.limit(20);
}
Example 3: Image optimization with sharp
npm install sharp
const sharp = require('sharp');
const fs = require('fs');
async function optimizeImage(inputPath, outputPath) {
await sharp(inputPath)
.resize(800, 600, { fit: 'inside', withoutEnlargement: true })
.jpeg({ quality: 80 })
.toFile(outputPath);
const inputSize = fs.statSync(inputPath).size;
const outputSize = fs.statSync(outputPath).size;
console.log(`Size reduction: ${((1 - outputSize / inputSize) * 100).toFixed(2)}%`);
}
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('image'), async (req, res) => {
const inputPath = req.file.path;
const outputPath = `optimized/${req.file.filename}.jpg`;
await optimizeImage(inputPath, outputPath);
res.json({ path: outputPath });
});
9. Compression
gzip
npm install compression
const compression = require('compression');
app.use(compression());
app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) return false;
return compression.filter(req, res);
},
level: 6
}));
Typical savings: HTML 70–90%, JSON 60–80%, CSS/JS 50–70%.
10. Common pitfalls
Event loop blocking
// Bad: CPU-heavy work on main thread
app.get('/heavy', (req, res) => {
let sum = 0;
for (let i = 0; i < 1e9; i++) sum += i;
res.json({ sum });
});
// Better: worker thread
const { Worker } = require('worker_threads');
app.get('/heavy', (req, res) => {
const worker = new Worker('./heavy-task.js');
worker.on('message', (result) => res.json({ sum: result }));
worker.on('error', (err) => res.status(500).json({ error: err.message }));
});
// heavy-task.js
const { parentPort } = require('worker_threads');
let sum = 0;
for (let i = 0; i < 1e9; i++) sum += i;
parentPort.postMessage(sum);
Memory leaks
Causes: Global accumulators, listeners never removed, unbounded caches.
const EventEmitter = require('events');
const emitter = new EventEmitter();
// Bad: listener added every request
app.get('/api/data', (req, res) => {
emitter.on('data', (data) => res.json(data));
});
// Better: remove after use
app.get('/api/data', (req, res) => {
const handler = (data) => {
res.json(data);
emitter.off('data', handler);
};
emitter.on('data', handler);
});
11. Practical tips
Request timing
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
if (duration > 1000) {
console.warn(`Slow: ${req.method} ${req.url} ${duration}ms`);
}
});
next();
});
Centralized async errors
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.get('/api/users', asyncHandler(async (req, res) => {
const users = await User.find();
res.json({ users });
}));
app.use((err, req, res, next) => {
logger.error(err.message, { stack: err.stack });
res.status(500).json({ error: 'Internal Server Error' });
});
Keep-Alive
const http = require('http');
const server = http.createServer((req, res) => res.end('Hello'));
server.keepAliveTimeout = 65000;
server.headersTimeout = 66000;
server.listen(3000);
Summary
Takeaways
- Clustering: Cluster module or PM2
-ifor multi-core - Caching: Redis, in-memory TTL, invalidate on writes
- Database: Indexes, avoid N+1, connection pools,
lean()where appropriate - Async:
Promise.all, limit concurrency withp-limit - Memory: Streams, LRU caches, monitor RSS/heap
- Compression:
compressionmiddleware, optimize images
Optimization priority
- Database queries (often the largest bottleneck)
- Caching (high impact, moderate effort)
- Async parallelism
- Compression (network)
- Clustering (throughput)
Tools
| Tool | Use |
|---|---|
--prof | CPU samples |
| Chrome DevTools | Heap and CPU |
| clinic.js | Holistic diagnosis |
| autocannon | HTTP load tests |
| PM2 | Process metrics |
Next steps
- Deeper Node.js security hardening
- Microservices patterns
Resources
Related posts
- Node.js Getting Started
- Node.js Modules
- Node.js Async Programming