Performance Basics
8 examples to get you started with Performance for Node.js backends - 6 basic and 2 intermediate.
Prerequisites
- Node.js 24.18.0 installed.
- A running HTTP service (Express 5 or Fastify 5) to benchmark against.
- Optional:
npm install -g clinicfor profiling (see clinic.js Suite).
Basic Examples
1. Measure Request Duration with hrtime
High-resolution timers beat Date.now() for latency measurement.
import { createServer } from "node:http";
const server = createServer((req, res) => {
const start = process.hrtime.bigint();
res.end("ok");
const elapsedMs = Number(process.hrtime.bigint() - start) / 1_000_000;
console.log(JSON.stringify({ path: req.url, durationMs: elapsedMs }));
});
server.listen(3000);process.hrtime.bigint()is monotonic - not affected by clock skew.- Log structured JSON so aggregators can compute p50/p95/p99.
- Handler timing excludes network RTT - pair with load tests for end-to-end SLOs.
Related: Load Testing - saturation and p95 under concurrency
2. Track p95 Latency in Application Code
Percentiles matter more than averages for API SLOs.
const durations: number[] = [];
function recordDuration(ms: number): void {
durations.push(ms);
if (durations.length > 10_000) durations.shift();
}
function p95(): number {
const sorted = [...durations].sort((a, b) => a - b);
const idx = Math.ceil(sorted.length * 0.95) - 1;
return sorted[idx] ?? 0;
}
// Expose via /metrics or log every N requests- Average latency hides slow tail requests that users feel.
- p95 is a common SLO target for internal APIs (p99 for customer-facing).
- In production, export histograms via OpenTelemetry or Prometheus - do not keep arrays in memory long term.
Related: Metrics that Matter - RPS, p95, error rate
3. Monitor Event Loop Delay
Loop lag delays every concurrent client, not just one slow route.
import { monitorEventLoopDelay } from "node:perf_hooks";
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();
setInterval(() => {
console.log(JSON.stringify({
event: "loop_delay",
p99Ms: histogram.percentile(99) / 1e6,
maxMs: histogram.max / 1e6,
}));
histogram.reset();
}, 10_000);monitorEventLoopDelaymeasures how late the loop runs scheduled work.- Rising p99 loop delay with moderate CPU signals sync blocking on the main thread.
- Alert before user-facing p95 doubles - loop lag is an early warning.
Related: Detecting Event-Loop Blockage - attribution tooling
4. Use performance.mark for Hot Paths
Built-in marks integrate with Chrome DevTools and clinic.js.
import { performance } from "node:perf_hooks";
async function fetchUser(id: string) {
performance.mark("fetchUser:start");
const user = await db.query("SELECT * FROM users WHERE id = $1", [id]);
performance.mark("fetchUser:end");
performance.measure("fetchUser", "fetchUser:start", "fetchUser:end");
return user;
}- Marks are cheap when used on bounded code paths, not every line.
performance.getEntriesByName("fetchUser")returns duration arrays for analysis.- Clear marks periodically in long-running processes to avoid memory growth.
Related: clinic.js Suite - flame graphs for hot paths
5. Baseline Throughput Before Tuning
Know requests-per-second at target concurrency before changing code.
# Quick smoke (not a full load test)
npx autocannon -c 50 -d 10 http://localhost:3000/health-c 50simulates 50 concurrent connections - closer to production than one curl.- Record RPS, p99 latency, and error rate as your baseline snapshot.
- Re-run after every optimization claim - numbers beat intuition.
Related: Load Testing - k6 scenarios and saturation points
6. Cap Payload Size at the Edge
Large JSON bodies block parsing synchronously on the main thread.
import express from "express";
const app = express();
app.use(express.json({ limit: "1mb" }));
app.post("/events", (req, res) => {
// body already bounded - reject oversized at parser
res.status(201).json({ ok: true });
});limitreturns 413 before your handler runs - saves CPU on abuse.- Pair with response shape diet - return only fields clients need.
- Streaming endpoints need different limits than JSON CRUD APIs.
Related: JSON Serialization Cost - response shape diet
Intermediate Examples
7. Production-Like Load Test Script
Sustained concurrency reveals blocks that single requests hide.
// k6 script - save as load-test.js
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
stages: [
{ duration: "1m", target: 50 },
{ duration: "3m", target: 50 },
{ duration: "1m", target: 0 },
],
thresholds: { http_req_duration: ["p(95)<200"] },
};
export default function () {
const res = http.get("http://localhost:3000/users");
check(res, { "status is 200": (r) => r.status === 200 });
sleep(0.1);
}- Ramp up gradually - instant spike traffic tests the load generator, not your app fairly.
thresholdsfail the run when p95 exceeds 200ms - gate merges on regression.- Run against staging with production-like data volume and connection pool sizes.
Related: Performance on Express - Express tuning under load
8. Heap Snapshot When Memory Grows
Suspected leaks need evidence before tuning --max-old-space-size.
import { writeHeapSnapshot } from "node:v8";
import { writeFileSync } from "node:fs";
function captureHeap(label: string): void {
const path = writeHeapSnapshot();
writeFileSync(`/tmp/heap-${label}.json`, ""); // marker file
console.log(JSON.stringify({ event: "heap_snapshot", label, path }));
}
// Call on SIGUSR2 or after N requests in staging
process.on("SIGUSR2", () => captureHeap("manual"));- Compare two snapshots in Chrome DevTools to find retained objects.
- Do not snapshot production under traffic - pauses the process.
- Fix leaks before raising heap limits - bigger heaps delay GC pain, not root cause.
Related: Memory & GC Tuning -
--max-old-space-sizeand GC flags
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.