Slow code rarely lives where you think it does. Developers spend hours optimising the loop they remember writing when the real bottleneck is a database query they did not notice, a serialisation call in a hot path, or a memory allocation pattern that hammers the garbage collector. The profiler is the tool that points you at the actual problem, not the imagined one. In 2026, profiling tooling is faster, more integrated, and available in production without a measurable overhead cost.
What changed in 2026
- Continuous profiling reached production. Pyroscope, Grafana Profiles, and Parca run low-overhead sampling profilers in production, so you profile real traffic rather than synthetic benchmarks.
- OpenTelemetry profiles signal is in beta — the same SDK that emits metrics, logs, and traces will soon emit profiles, making the correlation story complete.
- AI-assisted query analysis. Most managed databases (Neon, PlanetScale, Supabase) now have query advisor features that flag slow queries and suggest indexes automatically.
- V8 (Node.js/Chrome) and the JVM improved their profiling ergonomics — self-profiling with minimal flags, better symbolisation, and direct flamegraph output.
Where slowness actually comes from
| Category |
Typical cause |
Detection tool |
| Database |
N+1 queries, missing index, full scan |
Slow query log, EXPLAIN ANALYZE, ORM debug |
| CPU-bound |
Hot loop, expensive serialisation, regex |
CPU flamegraph |
| Memory/GC |
Large allocations in hot path, leaks |
Heap snapshot, allocation profile |
| Network I/O |
Sequential external calls |
Distributed trace |
| Lock contention |
Shared mutable state, DB row locks |
Profiler wall-time vs CPU-time gap |
CPU profiling — flamegraphs
A flamegraph shows call stacks sampled over time. Width = time spent. The widest frame near the top is your bottleneck.
Node.js:
# Built-in profiler
node --prof server.js
# ... run load test ...
node --prof-process isolate-*.log > profile.txt
# Or use Clinic.js for an interactive flamegraph
npx clinic flame -- node server.js
Python:
# py-spy — attaches to a running process without code changes
py-spy record -o profile.svg --pid 12345
# or during development
python -m cProfile -o output.prof myscript.py
snakeviz output.prof # interactive visualization
Go:
# pprof endpoint (add to your main)
import _ "net/http/pprof"
# then: go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
Database query profiling
N+1 is the most common production performance bug. Detect it in development:
// Prisma — log all queries
const prisma = new PrismaClient({
log: ['query'],
});
// Drizzle — log slow queries
const db = drizzle(client, { logger: true });
Identify and fix an N+1:
// BAD — N+1: 1 query for orders, then 1 query per order for user
const orders = await db.orders.findMany();
for (const order of orders) {
order.user = await db.users.findUnique({ where: { id: order.userId } });
}
// GOOD — 2 queries total (or 1 with a JOIN)
const orders = await db.orders.findMany({
include: { user: true },
});
Use EXPLAIN ANALYZE to understand query plans:
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM events
WHERE user_id = 123
ORDER BY created_at DESC
LIMIT 20;
Look for Seq Scan on large tables — add an index and confirm the plan switches to Index Scan.
Memory profiling
Node.js heap snapshot:
// In code (dev only)
const v8 = require('v8');
const fs = require('fs');
const snapshot = v8.writeHeapSnapshot();
// Open in Chrome DevTools Memory tab
Python memory profiler:
pip install memray
python -m memray run -o output.bin myscript.py
python -m memray flamegraph output.bin
Look for objects that accumulate over time without being GC'd — common in event listeners, caches without size bounds, and closures that hold references.
How to profile in production
Use a low-overhead continuous profiler rather than a full profiler that affects latency:
# Pyroscope sidecar (Kubernetes)
- name: pyroscope-agent
image: grafana/pyroscope:latest
env:
- name: PYROSCOPE_SERVER_ADDRESS
value: http://pyroscope:4040
- name: PYROSCOPE_APPLICATION_NAME
value: orders-service
Continuous profilers sample at ~100 Hz with < 1% CPU overhead — safe for production.
How to pick a profiling approach
- Latency spike you can reproduce? CPU flamegraph in development against a load test.
- Slow API endpoint, not sure why? Distributed trace first — see if it is DB, external call, or CPU.
- Memory growing over time? Heap snapshot before and after a traffic period; diff the retained objects.
- Production regression, can't reproduce locally? Continuous profiling (Pyroscope, Grafana Profiles) on the live service.
- Slow database queries? Enable slow query logging (> 100ms threshold), then
EXPLAIN ANALYZE on offenders.
Common mistakes
Profiling in the wrong environment. A profiler showing hot paths in a test with 10 records tells you nothing about production with 10 million. Profile against realistic data volumes.
Optimising without measuring the impact. Change one thing, re-measure, compare. A "fix" that does not show up in the profiler did not fix the bottleneck.
Fixing the wrong layer. An N+1 "fixed" in the application layer by batching inside the app is still an N+1 — fix it at the query layer with a JOIN or IN (...).
Ignoring p99 latency. Average latency looks fine while 1% of requests time out. Always look at percentiles (p95, p99) in your profiling data.
Not caching profile baselines. Save a flamegraph from the current release. When a regression lands, you have a before/after to diff.
What to skip
- Micro-benchmarking without context — nanosecond differences in a hot path rarely matter if the bottleneck is 100ms DB queries.
- Profiling with the debugger attached — debuggers add significant overhead and distort timing.
- Manual GC calls as a fix — explicit garbage collection masks allocation problems instead of fixing them.
FAQ
How do I find an N+1 in a codebase I did not write?
Enable ORM query logging in development and run the endpoint. Count the queries. Any count that scales with the number of returned rows is an N+1.
Is profiling safe in production?
Low-overhead sampling profilers (Pyroscope, py-spy, async-profiler) are designed for production. Full instrumentation profilers are not — use them in staging.
How long should I profile for?
Long enough to capture a statistically representative sample. For a loaded service, 30 seconds is usually enough for a CPU flamegraph; for memory leaks, profile across a full request cycle.
What is the difference between wall time and CPU time in a profiler?
Wall time includes waiting (I/O, locks, sleeps). CPU time is only active computation. A gap between the two points to I/O or contention as the bottleneck, not computation.
Where to go next
See How to monitor a service in 2026, How to optimize SQL queries in 2026, and Logging explained in 2026.