Almost every slow Express 24,430 route is slow for one of a handful of reasons, and the commonest is startup work done per request. The route below reads a catalog file, parses it and filters it on every call; the fix does all three once. Both handlers live in the same app, so one load test measures both.
app.get('/slow', (req, res) => { // read, parse, filter: every request
const all = JSON.parse(fs.readFileSync('catalog.json', 'utf8'));
res.json({ data: all.filter((b) => b.year > 1980) });
});
const all = JSON.parse(fs.readFileSync('catalog.json', 'utf8')); // 400 records, once
const modern = all.filter((b) => b.year > 1980);
app.get('/fast', (req, res) => res.json({ data: modern }));$ npx autocannon -c 50 -d 5 http://127.0.0.1:4303/slow │ Latency │ 14 ms │ 22 ms │ 33 ms │ 36 ms │ 23 ms │ ... │ Req/Sec │ 2,067 │ 2,067 │ 2,137 │ 2,163 │ 2,125 │ ... $ npx autocannon -c 50 -d 5 http://127.0.0.1:4303/fast │ Latency │ 3 ms │ 6 ms │ 10 ms │ 12 ms │ 6.01 ms │ ... │ Req/Sec │ 6,491 │ 6,491 │ 8,079 │ 8,327 │ 7,690.8 │ ...
Moving two lines out of the hot path took the i9-7980XE from 2,125 to 7,691 requests per second — 3.6 times the throughput — and the median latency from 22 ms to 6 ms. readFileSync blocks the event loop, so while one request waits on the disk every other connection waits too, which is why the 99th percentile (36 ms) sits so far above the median. Any synchronous call in a handler behaves this way: fs.*Sync, crypto.pbkdf2Sync, zlib.gzipSync, JSON.parse of a megabyte.
| Mistake | Why it hurts | Fix |
|---|---|---|
| Sync I/O or crypto in a handler | blocks every connection | async API, worker thread |
| Middleware mounted app-wide | runs for unrelated routes | mount it on one router |
| console.log per request | synchronous write to a TTY | pino 18,227 (Structured Logs with Pino) |
| Body parsing before limits | you parse attacker payloads | limiter first (Rate Limiting) |
| Unbounded list page size | one client asks for it all | cap limit (Pagination) |
| NODE_ENV left unset | no view cache, verbose errors | set it to production |
The last row is free money in a server-rendered app: NODE_ENV=production makes Express cache compiled templates instead of recompiling on every render. Profile before guessing — Measure Before You Optimize finds the hot frame.