NestJS Profiler

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.

Profiler home — the sidebar of views with count badges and the HTTP list narrowed by the method filter

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.

GraphQL view listing QUERY and MUTATION operations alongside their status

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 detail tab with operation type, name, syntax-highlighted query and variables (Query)

GraphQL detail tab for a Mutation with input variables

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.

Exceptions tab showing an amber GraphQLError badge with its BAD_REQUEST code, validation message and location

Built-in tabs

The Request tab details the method, URL, headers, and collected body.

Request tab showing a POST create with its method, headers and JSON request body

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

Response tab showing status, headers, and JSON body

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.

Execution Trace with the automatic instrumentation on: the call tree from the controller down to the SQL statement, the All / I/O only / Code only lens, and the table below mirroring the bars

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.

Performance tab with duration, CPU time, heap delta, event-loop utilization and the execution trace of the recorded spans

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.

Logs tab showing messages with context names and a structured JSON payload

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.

Exceptions tab displaying a captured InternalServerErrorException with its stack trace

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.

Database tab showing a profiled SQL SELECT query

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:

Database tab of a PATCH that affected 0 rows, with a "Performance — 1 issue detected" banner reading "No rows" and the query showing 0 rows

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

MongoDB tab showing a profiled Mongoose query with collection, operation and duration

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.

HTTP Client tab showing a GET call to JSONPlaceholder

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

Cache tab showing a GET_HIT and a 100 percent 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.

Command tab showing a profiled nest-commander run with its arguments and options

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.

RabbitMQ view showing a consumed review.created delivery and its filters

Message tab showing a consumed review.created delivery with exchange, routing key, handler 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.

Security tab showing an authenticated demo_user with the admin role

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

Validator tab showing two validation violations on CreatePostDto

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.

Config view showing the runtime tiles and the flattened app/database keys with database.password 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.

Runtime view with heap, resident set, CPU and event-loop lag tiles, the sampled trends, the garbage-collection breakdown and the V8 heap spaces

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).

Discover / HTTP view listing the application's registered routes with their controllers and handlers

Powered & maintained by

On this page