Node servers
Any Node HTTP server, whatever the framework: Express, Fastify, Koa, Hapi, NestJS, Hono on
@hono/node-server, with Prisma 7 and a driver adapter. Your server’s code does not change: a
preload in the dev command profiles each request, and the profiler’s page gets a port of its own.
Complete apps are in
examples/express and
examples/fastify.
1. Install it as a dev dependency
Section titled “1. Install it as a dev dependency”npm i -D query-profilerA production install (npm ci --omit=dev) leaves it out, and nothing in production loads it.
2. Profile the Prisma client
Section titled “2. Profile the Prisma client”The package is only there in development, so import it only if it is installed. See Prisma for what the two calls do.
import { PrismaPg } from "@prisma/adapter-pg";import { PrismaClient } from "./generated/prisma/client.js";
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });// A dev dependency: a production install leaves it out, and the client stays plain.const profiler = await import("query-profiler/prisma").catch(() => undefined);
export const prisma: Omit<PrismaClient, "$on"> = profiler ? profiler.profilePrisma(new PrismaClient(profiler.withProfiler({ adapter }))) : new PrismaClient({ adapter });With a bundled server, plain imports work too: see installation.
3. Add the preload to your dev command
Section titled “3. Add the preload to your dev command”One flag for Node, and nothing else:
node --import query-profiler/register src/server.jsIt profiles each request from Node itself, before your framework sees it, and serves the profiler.
It also gives your app’s own imports of the profiler (query-profiler/prisma in db.ts) its real
code. Without it, as in production, every entry is the version that does nothing.
With your watcher:
{ "scripts": { "dev": "node --watch --import query-profiler/register src/server.js", "start": "node src/server.js" }}{ "scripts": { "dev": "tsx watch --import query-profiler/register src/server.ts", "start": "node dist/server.js" }}{ "scripts": { "dev": "nodemon --exec \"node --import query-profiler/register\" src/server.js", "start": "node src/server.js" }}{ "scripts": { "start:dev": "nest start --watch --exec \"node --import query-profiler/register\"", "start:prod": "node dist/main" }}Put the flag in NODE_OPTIONS, and every Node process the command starts gets it:
NODE_OPTIONS="--import query-profiler/register" npm run devThe profiler opens its port only in the process whose server listens, so a CLI that launches your app never takes it.
The start script stays as it is: there is nothing to remove before deploying.
4. Open the profiler
Section titled “4. Open the profiler”Run the dev server. Once your app listens, the terminal says where the profiler is:
[query-profiler] http://localhost:4983/_profilerIt shows every request, live. Port 4983 is the default; set QUERY_PROFILER_PORT to pick another.
If the port is busy, the profiler waits a second for a restarting app to let go of it, then takes the
next free one and prints it. In a container, set QUERY_PROFILER_HOST=0.0.0.0 and publish the port:
see troubleshooting.
Several processes, one profiler. With Node’s cluster, PM2 in cluster mode, or two apps on the
same machine, the first process to start serves the profiler, and the others send it their requests:
[query-profiler] http://localhost:4983/_profiler (shared with another process)If the process that serves it stops (a restart, a worker that dies), another one takes the port over.
To keep two apps apart, give each its own QUERY_PROFILER_PORT.
If it is off, the terminal says why: a NODE_ENV other than development, or a Node older than
22.19 or 24.5. Nothing at all, or an empty profiler? See troubleshooting.
Seeing what your API did
Section titled “Seeing what your API did”-
The page at
http://localhost:4983/_profiler: every request, with its queries, operations, timeline and warnings. -
X-Debug-Tokenon every response: open/_profiler/<token>for that request. -
One line per request in the terminal, with
QUERY_PROFILER_LOG=1:[query-profiler] GET /users 200 · 7 queries · 18.92 ms in db · 7 operations · 1 warning · 80.49 ms · 90dd08 -
The JSON behind the page: see the HTTP API.
Frameworks
Section titled “Frameworks”Any framework that serves through Node’s http module works the same way. These are tested in CI,
with Prisma against PostgreSQL:
| Framework | Tested with |
|---|---|
| Express | 4.18 and later, 5 |
| Fastify | 4.26 and later, 5 |
| Koa | 3 |
| Hapi | 21 |
| NestJS | 12, on Express |
| Hono | 4, on @hono/node-server 2 |
Before Express 4.18, its body parser loses the request’s async context: a route that reads a body runs outside its profile, and its queries are lost.
HTTP/2 servers (Fastify with http2: true, say) are profiled on Node 22.17+ and 24.2+, where Node
publishes their requests too. Their responses carry no X-Debug-Token.
When a request comes from another origin, the preload adds X-Debug-Token to
Access-Control-Expose-Headers, so a frontend there can read it. If your CORS settings set that
header themselves (exposedHeaders in cors), list X-Debug-Token there too.
How it works
Section titled “How it works”Node publishes each request on its http.server.request.start diagnostics channel, right before the
server hands it to its handler. The preload opens the request’s profile there, so your framework,
and every query it makes, runs inside it. More in how it works.