A production debugging story with real code, real numbers, and every mistake I made along the way.
It started with a Slack message on a Tuesday afternoon:
“Hey, the dashboard is loading really slowly. Like, really slowly.”
Our internal analytics API was taking anywhere between 1.8s and 2.4s to respond.
Users had complained. The tech lead had noticed. It was time to fix it.
I spent two days profiling, debugging, and fixing five separate issues. This article walks through each one — with real code, real numbers, and honest mistakes.
“Performance problems are almost never where you think they are. Profile first. Always.”
Step 0: Baseline measurement
Before touching any code, I needed to know where time was actually being spent.
I started with simple timing:
async function timedRoute(label, fn) {
const start = performance.now();
const result = await fn();
const ms = (performance.now() - start).toFixed(2);
console.log(`[PERF] ${label}: ${ms}ms`);
return result;
}
// Usage
const user = await timedRoute('fetchUser', () => getUser(id));
const orders = await timedRoute('fetchOrders', () => getUserOrders(id));
const metrics = await timedRoute('computeMetrics', () => buildMetrics(orders));
const config = await timedRoute('loadConfig', () => getConfig());
Results
fetchUser => 12ms => ✅ OK
fetchOrders => 1340ms => 🔴 Problem
computeMetrics => 380ms => 🔴 Problem
loadConfig => 260ms => 🔴 Problem
Three separate bottlenecks.
Problem 1: N+1 Query (The Biggest Killer)
❌ Before
async function getUserOrders(userId) {
const orders = await db.query(
'SELECT * FROM orders WHERE user_id = ?', [userId]
);
for (const order of orders) {
order.items = await db.query(
'SELECT * FROM order_items WHERE order_id = ?', [order.id]
);
}
return orders;
}
If user has 50 orders → 51 queries
After (Single JOIN)
async function getUserOrders(userId) {
const rows = await db.query(`
SELECT
o.id, o.created_at, o.status, o.total,
i.id AS item_id, i.product_id, i.quantity, i.unit_price
FROM orders o
LEFT JOIN order_items i ON i.order_id = o.id
WHERE o.user_id = ?
ORDER BY o.created_at DESC
`, [userId]);
const ordersMap = new Map();
for (const row of rows) {
if (!ordersMap.has(row.id)) {
ordersMap.set(row.id, {
id: row.id,
createdAt: row.created_at,
status: row.status,
total: row.total,
items: []
});
}
if (row.item_id) {
ordersMap.get(row.id).items.push({
id: row.item_id,
productId: row.product_id,
quantity: row.quantity,
unitPrice: row.unit_price
});
}
}
return [...ordersMap.values()];
}
Result
- 1340ms → 42ms
- Queries reduced: ~50 → 1
Problem 2: Blocking the Event Loop (readFileSync)
❌ Before
function loadFeatureFlags() {
const raw = fs.readFileSync('/etc/app/feature-flags.json', 'utf-8');
return JSON.parse(raw);
}
This blocks the entire Node.js event loop.
After (Async + Cache)
let _flagsCache = null;
let _lastLoaded = 0;
const CACHE_TTL = 60000;
async function loadFeatureFlags() {
const now = Date.now();
if (_flagsCache && (now - _lastLoaded) < CACHE_TTL) {
return _flagsCache;
}
const raw = await fs.promises.readFile('/etc/app/feature-flags.json', 'utf-8');
_flagsCache = JSON.parse(raw);
_lastLoaded = now;
return _flagsCache;
}
Result
- 260ms → ~0.1ms
Problem 3: CPU Blocking (Heavy Computation)
Issue
computeMetrics was CPU-heavy → blocking event loop.
Solution A: Redis Cache
async function getMetrics(userId) {
const cacheKey = `metrics:user:${userId}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const orders = await getUserOrders(userId);
const metrics = await buildMetrics(orders);
await redis.setEx(cacheKey, 300, JSON.stringify(metrics));
return metrics;
}
Solution B: Worker Threads
import { workerData, parentPort } from 'worker_threads';
const { orders } = workerData;
function buildMetrics(orders) {
const totals = orders.map(o => o.total).sort((a, b) => a - b);
const p50 = totals[Math.floor(totals.length * 0.50)];
const p95 = totals[Math.floor(totals.length * 0.95)];
const revenue = totals.reduce((s, v) => s + v, 0);
return { p50, p95, revenue, count: orders.length };
}
parentPort.postMessage(buildMetrics(orders));
Result
- 380ms → 2ms (cached)
- No event loop blocking
What I Learned
**1. Profile before guessing
- N+1 queries hide easily
- Sync functions are dangerous
- Caching needs strategy (TTL + invalidation)
- Event loop is everything in Node.js**
A 96% latency reduction from five focused fixes.
Not a rewrite. Not a new language. Just smart debugging.
Top comments (0)