Profiler UI
A visual tour of the profiler web interface - the profiles list, the built-in tabs, and every collector panel.
Open /_profiler to browse the list of captured profiles, then click any row to open its detail view. The detail view is organized into built-in tabs (request, response, performance, logs, exceptions) plus one tab per active collector. The screenshots below come from the example application with every collector enabled.
This page is a visual tour of the panels and tabs. For the reference side — the UI endpoints, the debug response headers, the filter query parameters and how to export a profile — see Browsing profiles.
Profiles list
/_profiler opens on a sticky sidebar of views. Each entrypoint kind (HTTP, GraphQL, Commands, RabbitMQ, Events) gets its own page under a Profiling group, and every global-scope collector is a view of its own — grouped under a heading when it belongs to a family (Discover, one view per transport; Schemas, one per ORM), flat otherwise (Runtime, Config). Each is badged with its count. Only the active view is rendered; it is picked server-side from a ?view= link, so there is no client-side routing. Process-wide memory and CPU live in their own Runtime view rather than above the lists — a trend wants an axis made of time, not of traffic.
Every list carries its own filter bar and is filtered independently: the universal filters (search, duration, performance tag — slow / N+1 / chatty / large payload / no rows — plus an Exception select and an Errors checkbox), the HTTP-status filters (status, status class) shown only on the kinds that carry an HTTP response (HTTP, GraphQL), and filters specific to that kind (e.g. HTTP method, RabbitMQ delivery/exchange/handler). A kind hides a universal filter that is redundant for it — the Commands list has no Errors checkbox, its Status filter already asking the same question.

GraphQL operations
Requires @eleven-labs/nest-profiler-graphql - see the GraphQL support section in Getting started.
GraphQL operations get their own GraphQL view in the sidebar (separate from REST requests), each row showing the operation type (QUERY, MUTATION, or SUBSCRIPTION) and the operation or field name. Its filter bar adds an Operation filter to narrow the list by operation type.

Opening an operation lands on its dedicated GraphQL detail tab, displaying:
- Operation type badge (Query / Mutation / Subscription)
- Operation name if present in the request
- Field name - the entry-point resolver
- Query - syntax-highlighted GraphQL document
- Variables - the variables object, syntax-highlighted as JSON
- Response - the
{ data, errors }envelope returned to the client - Request headers - the underlying HTTP headers


GraphQL-level errors (schema validation failures, resolver errors returned in response.body.errors) appear in the Exceptions tab with an amber GraphQLError badge, visually distinct from NestJS runtime exceptions which use a red badge. Since GraphQL names every error GraphQLError, its extensions.code is shown alongside — BAD_REQUEST here — and that code is what the Exception filter lists and what decides whether the operation counts as an error.

Built-in tabs
The Request tab details the method, URL, headers, and collected body.

The Response tab shows the status, response headers, and body returned by the handler.

The Performance tab groups everything about what the execution cost. It leads with four figures: the request duration (also badged on the tab), the CPU time with its share of that duration, the heap at the end with how much the request moved it, and the event-loop utilization over the window.
The CPU share is the one that changes what you do next. Near 100% the request was computing, near 0% it was waiting on something — and a slow endpoint is fixed very differently in the two cases, so the tab names it rather than leaving you to divide: CPU-bound, waiting on I/O, or mixed. Under Resource usage below the tiles sit the user/system split, the resident set and its delta, the heap before and after, and the garbage collections that ran during the request — the explanation for a latency spike a duration alone cannot give.
The execution trace closes the tab: every timed operation of the request on one axis — the entrypoint, framework phases, outgoing calls, database queries and the spans you opened with tracer.span() — nested by causality, foldable, each bar showing its self-time and deep-linking to the panel row holding its detail. Above it, a flat Request Lifecycle band breaks the request into its framework phases.
With the optional automatic instrumentation on, the trace also carries one bar per provider method call — the full call tree, controller down to repository, with the query nested under the method that issued it. Those bars answer "who issued this query" and are noise when the question is "which query is slow", so the panel offers a lens over the same tree — All, I/O only, Code only. It filters the bars and the table under them together, and a hidden bar leaves its children attached to their real parent: you change how much of the trace you see, never which trace.
Two controls sit beside it and apply to any trace. Critical path dims everything off the chain that decided the total — from the root, the child that finishes last, all the way down; shortening a span off that chain moves nothing, which is what makes it the place to look first. Hide under drops spans below a threshold, defaulting to the traceMinDuration module option — 0, show everything, because a developer who cannot find a span they know they opened has no reason to suspect a threshold.

One thing to know before reading a number: these are process-wide deltas over the request's window, not an isolated measurement of that one execution. Node runs a single thread, so under concurrent traffic a request is charged with what its neighbours spent too — which is why a CPU share above 100% reads includes concurrent work rather than claiming the request was CPU-bound. The tab says so under the figures, and CPU and memory has the full picture.

The Logs tab lists entries captured through createProfilerLogger(). Each row shows the message first - with structured payloads rendered as a JSON block under it - then the logger context name. See the Log capture page for the supported logger conventions.

The Exceptions tab lists what the profile captured, each with its message and stack trace — here the InternalServerErrorException behind a 500. A captured exception is not automatically an error, though: a BadRequestException answering 400 means the application did its job, so it appears here and under the Exception filter, but not under the Errors checkbox. See What counts as an error.

Collectors
The Database collector shows SQL queries (TypeORM or MikroORM) with their SQL type and duration, plus a metadata line per query — rows affected/returned and the connection, rendered as a chip (localhost:5432 / shop). The panel header sums the rows read.

A zero-row UPDATE/DELETE is a silent failure — the query ran but changed nothing (a mismatched WHERE). It is highlighted, its 0 rows count turns amber, and the zero-rows tag lights the Performance banner and colours the Database tab:

The MongoDB collector (requires @eleven-labs/nest-profiler-mongoose) shows Mongoose queries and aggregations with their operation, collection, duration, result/affected count, and connection.

The HTTP Client collector shows the outgoing calls the request made, whichever client issued them — the bundled axios and fetch adapters, or one of your own. With a phases provider installed, each row also carries a stacked Phases bar breaking the duration into DNS, handshake, time-to-first-byte and download.

The Cache collector shows GET_HIT, GET_MISS, SET operations and the hit rate.

The Command collector (requires @eleven-labs/nest-profiler-commander) profiles nest-commander CLI runs. With file storage, commands get their own Commands view — a CLI badge and the command line in place of the URL, with a Status filter for successful vs failed runs — and the Command tab reconstructs the full invocation, then details the positional arguments and the parsed options separately.

The RabbitMQ collector (requires @eleven-labs/nest-profiler-rabbitmq) profiles messages consumed via @RabbitSubscribe. Each delivery becomes its own profile: a dedicated RabbitMQ view, and a Message detail tab showing the exchange, routing key, handler, delivery metadata and JSON payload.


The RabbitMQ publish collector (same package, RabbitMqPublishCollectorModule) covers the other direction: the messages the profiled request published through AmqpConnection.publish, one row each with its exchange, routing key, message properties, headers, payload, duration and outcome — plus a copy button holding a runnable channel.publish(...) snippet.
The Events collector (requires @eleven-labs/nest-profiler-event-emitter) covers @nestjs/event-emitter in both directions: the Events panel lists every emit / emitAsync the profiled execution made, and each @OnEvent handler run becomes an event profile of its own — with its own Events view in the sidebar, a Status filter, and every other panel (Logs, Database, HTTP Client…) describing what that handler did rather than what the publishing request did.
The Security collector shows the authenticated user, roles, and decoded JWT claims.

The Validator collector shows validated DTOs and failing class-validator constraints.

The Config collector is global — it describes the application, not one request — so it is a sidebar view of its own, exposing the runtime and the flattened configuration with secrets masked.

The Runtime view is global as well, and the only one whose content comes from elapsed time rather than from a captured execution. The per-request figures on the Performance tab say what one request cost; they cannot say whether the heap has been climbing all afternoon, whether the loop is being blocked, or whether a major collection runs every few seconds — a leak is a shape over time, not a value on one request.
So this view is that shape: the heap against its own limit, the resident set, CPU as a share of one core and the event-loop lag p99 as headline tiles, then trends for heap, RSS, CPU and lag over the sampled window, garbage collection split into minor / major / incremental with its pause time, the V8 heap spaces, and the process facts (pid, uptime, Node version, CPU count, load average). Sampling is on by default every 5 s; runtime: false removes the view and stops the sampler. Everything it reads comes from node:os, node:v8 and node:perf_hooks — nothing to install, nothing leaving the process.

The Discover views (requires @eleven-labs/nest-profiler-routes) are global too: one view per transport, each listing that transport's routing table as discovered at startup — method, path, controller/handler, a lock on guarded routes, and the per-route inputs when a row is expanded. A transport that discovered nothing gets no view, and the keys are namespaced (?view=discover-graphql) so they never collide with the GraphQL profile list.
A transport whose static surface is more than a list of handlers puts it above them, as titled sections: Discover / RabbitMQ opens on the declared topology — connections (URIs masked), exchanges, queues with the binding and the x-… arguments that feed them — then lists each consumer, expanding to its whole subscription (queue, routing keys, connection, queue options, error behaviour).

Example application
Full-featured NestJS application demonstrating every profiler collector, organized into one module per collector package.
Tutorials
Step-by-step NestJS profiler tutorials - enable collectors for SQL, HTTP, GraphQL, cache and auth, and build your own custom collector and entrypoint types.