NestJS Profiler
Packagesnest-profiler

Performance tags (N+1, slow, error…)

Detect N+1 queries, slow queries, failed HTTP calls, chatty endpoints, large payloads and silent zero-row writes with the core rule-based tagging engine, and filter profiles by tag.

The profiler runs a single analysis pass once per profile — after every collector, before the profile is persisted — that flags common performance anti-patterns and attaches structured tags to the offending queries and HTTP calls. Tags surface as coloured pills in the panels and on the list page, where a Performance tag filter narrows the profiles down to a given issue.

Detection spans every query collector (typeorm, mikro-orm, mongoose) and the outgoing-HTTP collector (http). Grouping is done once in the core on a per-entry fingerprint each collector supplies (normalized SQL, collection + operation + filter shape, or method + normalized URL), so the same query executed with different bind values is recognised as a repeat.

The detail page leads with a Performance banner summarising the issues, and colour-codes the affected collector tab (and its sub-tabs) by severity so a problematic panel stands out at a glance. Below, a GraphQL query that lists products and resolves each product's reviews from MongoDB (a classic N+1) is flagged N+1 ×4:

Profile detail — a Performance banner reading "1 issue detected" with an N+1 ×4 tag, above a Database panel whose MongoDB sub-tab is marked with a red severity dot

Built-in tags

TagApplies toWhen
slowqueries, HTTPA single call's duration reaches the collector's slowThreshold.
n-plus-onequeries, HTTPThe same fingerprint runs nPlusOneThreshold+ times — the N+1 anti-pattern (repeated queries or repeated HTTP calls); the pill reads N+1 ×N.
errorqueries, HTTP, profileA call failed (it threw, or answered ≥ 500), or the profile itself failed per its kind's definition. Unlike the tags around it, "failure" has no universal meaning, so it is defined per package — see What counts as an error.
chattyprofileA single request made a lot of calls to one backend — chattyThreshold+ SQL/Mongo queries or outgoing HTTP calls (default 20 queries, 10 HTTP). Unlike N+1, the calls need not be identical: it catches an endpoint that is "chatty" overall (many small round-trips), often a sign of missing batching or eager-loading.
large-payloadHTTPA request/response payload reaches largePayloadThreshold.
zero-rowsqueriesA write changed nothing — a SQL UPDATE/DELETE with rowCount === 0, or a Mongoose delete/update with count === 0. A silent failure (typically a mismatched WHERE / filter). Empty reads and writes whose row count could not be captured are never flagged.

Each tag is a ProfilerTag{ id, label, severity, count?, detail? } — attached to the entry and aggregated (deduplicated) onto profile.tags.

Thresholds

Thresholds live on each collector, so a Mongo slow query and a slow HTTP call can differ. Defaults:

Optiontypeorm / mikro-orm / mongoosehttp
slowThreshold (ms)100300
nPlusOneThreshold22
chattyThreshold2010
largePayloadThreshold1 MB
TypeOrmCollectorModule.forRoot({ slowThreshold: 50, nPlusOneThreshold: 2 });
HttpCollectorModule.forRoot({ slowThreshold: 500, largePayloadThreshold: 512 * 1024 });

Migrating from slowQueryThreshold — the option was renamed to slowThreshold, and the per-entry isSlow boolean was removed. "Slow" is now the slow tag, computed centrally by the engine (no longer at capture time); read it from entry.tags or profile.tags.

Configuring severity

Each tag carries a severity ('info' | 'warning' | 'danger') that drives its colour everywhere in the UI — the pill, the duration text, the summary counts, the row highlight and the Performance banner. Every threshold-based tag's severity is configurable per collector, as a flat option next to its threshold:

OptionApplies toDefault
slowSeverityqueries, httpwarning
nPlusOneSeverityqueries, httpdanger
chattySeverityqueries, httpwarning
largePayloadSeverityhttpwarning
zeroRowsSeveritytypeorm / mikro-orm / mongoosewarning
// Treat a slow query as a hard failure in this app — the pill, duration, count and banner all turn red.
TypeOrmCollectorModule.forRoot({ slowThreshold: 50, slowSeverity: 'danger' });

The error tag is danger by default and is configurable like the others, but through its own option — what earns it is not a duration threshold but a definition of failure, which each package owns: ProfilerModule.forRoot({ error: { severity: 'warning' } }). See What counts as an error. A custom rule sets its own severity directly on the tag it emits (see below).

Filtering by tag

The list page shows a Performance tag select (Slow / N+1 / Chatty / Large payload / No rows) that keeps only the profiles carrying that tag, with the tag pills rendered inline on each row. Failures are not performance issues, so the error tag has its own Errors checkbox, alongside an Exception select that narrows to one failure type — see What counts as an error.

Profiles list filtered by the N+1 performance tag, showing a single GraphQL products query row tagged N+1 ×4

Custom rules

A rule inspects the collected entries and tags whatever it wants. Register one declaratively or programmatically:

import { PerformanceRule } from '@eleven-labs/nest-profiler';

const seqScanRule: PerformanceRule = {
  id: 'seq-scan',
  evaluate(ctx) {
    for (const { entries, domain } of ctx.collectors) {
      if (domain !== 'query') continue;
      for (const entry of entries) {
        if (/seq scan/i.test((entry as { sql?: string }).sql ?? '')) {
          ctx.tagEntry(entry, { id: 'seq-scan', label: 'Seq scan', severity: 'warning' });
        }
      }
    }
  },
};

ProfilerModule.forRoot({ performance: { rules: [seqScanRule] } });
// …or, from a module's onModuleInit: core.registerPerformanceRule(seqScanRule);

A rule's ctx.collectors entries carry duration, fingerprint and the concrete collector fields (cast by domain); call ctx.tagEntry(entry, tag) or ctx.tagProfile(tag). The emitted tag id becomes filterable — offer it in the list select with core.registerFilterOption('tag', { value: 'seq-scan', label: 'Seq scan' }).

Powered & maintained by

On this page