npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pawells/nestjs-pyroscope

v2.0.2

Published

NestJS Pyroscope profiling with decorators and interceptors

Readme

NestJS Pyroscope Module

GitHub Release CI npm version Node License: MIT GitHub Sponsors

Pyroscope continuous profiling integration for NestJS with decorators, interceptors, and health monitoring. Provides automatic profiling of HTTP requests and methods with minimal setup.

Installation

yarn add @pawells/nestjs-pyroscope @pyroscope/nodejs

Requirements

  • Node.js: >= 22.0.0
  • NestJS: >= 10.0.0
  • @pyroscope/nodejs: ^0.4.10

Peer Dependencies

{
  "@nestjs/common": ">=10.0.0",
  "@nestjs/terminus": ">=10.0.0",
  "rxjs": ">=7.0.0"
}

Overview

nestjs-pyroscope integrates Pyroscope continuous profiling into NestJS. It depends on @pawells/nestjs-shared and provides:

  • PyroscopeService: Core service for profiling lifecycle management
  • Profiling Decorators: @Profile, @ProfileMethod, @ProfileAsync for selective method profiling
  • ProfilingInterceptor: Global HTTP request profiling across all endpoints
  • Built-in Health Endpoints: /profiling/health, /profiling/metrics, /profiling/status, /profiling/metrics/prometheus
  • ProfilingHealthIndicator: Integration with NestJS health checks
  • MetricsService: Aggregation and export of profiling metrics (JSON and Prometheus formats)
  • Utilities: Configuration validation, tag formatting, metric aggregation, error handling

Quick Start

1. Module Setup (Synchronous)

import { Module } from '@nestjs/common';
import { PyroscopeModule } from '@pawells/nestjs-pyroscope';

@Module({
  imports: [
    PyroscopeModule.ForRoot({
      isGlobal: true,
      config: {
        enabled: process.env.NODE_ENV === 'production',
        serverAddress: 'http://localhost:4040',
        applicationName: 'my-nestjs-app',
        tags: {
          version: '1.0.0',
          environment: process.env.NODE_ENV,
        },
      },
    }),
  ],
})
export class AppModule {}

2. Module Setup (Asynchronous)

Use ForRootAsync when your configuration depends on other providers:

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PyroscopeModule } from '@pawells/nestjs-pyroscope';

@Module({
  imports: [
    ConfigModule.forRoot(),
    PyroscopeModule.ForRootAsync({
      isGlobal: true,
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => ({
        enabled: configService.get('PROFILING_ENABLED') === 'true',
        serverAddress: configService.get('PYROSCOPE_SERVER_ADDRESS'),
        applicationName: configService.get('APP_NAME'),
        environment: configService.get('NODE_ENV'),
        tags: {
          version: configService.get('APP_VERSION'),
          region: configService.get('REGION'),
        },
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

Usage

Profiling Decorators

The package provides three decorators for different profiling scenarios. All require PyroscopeService to be injected.

@Profile - Class-level Decorator

Profiles all methods in a class automatically.

When to use:

  • You want profiling on all methods without per-method decoration
  • Minimal setup required

Important: The injected PyroscopeService property must be named pyroscopeService on the class instance. The decorator looks for this.pyroscopeService at runtime.

import { Injectable } from '@nestjs/common';
import { Profile, PyroscopeService } from '@pawells/nestjs-pyroscope';

@Profile({ tags: { service: 'user' } })
@Injectable()
export class UserService {
  constructor(private pyroscopeService: PyroscopeService) {}

  async findById(id: string) {
    // Automatically profiled as 'UserService.findById'
    return this.database.users.findOne(id);
  }

  async update(id: string, data: any) {
    // Automatically profiled as 'UserService.update'
    return this.database.users.update(id, data);
  }

  getCount() {
    // Sync methods also profiled
    return this.database.users.count();
  }
}

@ProfileMethod - Method-level Decorator

Profiles specific methods with optional custom names and tags.

When to use:

  • You want selective profiling of specific methods
  • You need custom profile names or method-level tags

Important: The injected PyroscopeService property must be named pyroscopeService on the class instance.

import { Injectable } from '@nestjs/common';
import { ProfileMethod, PyroscopeService } from '@pawells/nestjs-pyroscope';

@Injectable()
export class OrderService {
  constructor(private pyroscopeService: PyroscopeService) {}

  @ProfileMethod({ name: 'order.create.expensive' })
  async createOrder(orderData: any) {
    // Profiled with custom name 'order.create.expensive'
    return this.processPayment(orderData);
  }

  @ProfileMethod({ tags: { operation: 'query' } })
  async findActiveOrders() {
    // Profiled with custom tags
    return this.database.orders.find({ status: 'active' });
  }

  @ProfileMethod()
  calculateTotal(items: any[]) {
    // Sync method profiling with default name 'OrderService.calculateTotal'
    return items.reduce((sum, item) => sum + item.price, 0);
  }
}

@ProfileAsync - Async Method Decorator

Specifically designed for async/Promise-based methods.

When to use:

  • You have async methods and want guaranteed Promise handling
  • You prefer explicit async decoration
  • You want accurate timing for Promise-based operations

Important: The injected PyroscopeService property must be named pyroscopeService on the class instance.

import { Injectable } from '@nestjs/common';
import { ProfileAsync, PyroscopeService } from '@pawells/nestjs-pyroscope';

@Injectable()
export class ApiService {
  constructor(private pyroscopeService: PyroscopeService) {}

  @ProfileAsync({ name: 'api.fetch.user-profile' })
  async fetchUserProfile(userId: string) {
    // Guaranteed async/await handling with proper timing
    const response = await fetch(`https://api.example.com/users/${userId}`);
    return response.json();
  }

  @ProfileAsync({ tags: { endpoint: 'search' } })
  async search(query: string) {
    // Async profiling with custom tags
    return await this.elasticsearch.search(query);
  }
}

ProfilingInterceptor - Global Request Profiling

Automatically profiles all HTTP requests with no additional decorators needed.

Features:

  • Captures HTTP method, path, and status code
  • Records timing from request start to response
  • Distinguishes between successful and failed requests
  • Includes User-Agent header

When to use:

  • You want automatic profiling of all HTTP requests
  • You need comprehensive request performance metrics
  • You want global observability without per-endpoint decoration
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ProfilingInterceptor } from '@pawells/nestjs-pyroscope';

@Module({
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: ProfilingInterceptor,
    },
  ],
})
export class AppModule {}

Example profiling profile names generated by the interceptor:

  • HTTP GET /users/:id
  • HTTP POST /orders
  • HTTP DELETE /users/:id/sessions

Manual Profiling with PyroscopeService

For fine-grained control, use PyroscopeService directly:

import { Injectable } from '@nestjs/common';
import { PyroscopeService, IProfileContext } from '@pawells/nestjs-pyroscope';

@Injectable()
export class DataProcessingService {
  constructor(private readonly pyroscope: PyroscopeService) {}

  async processLargeDataset(data: any[]) {
    // Manual profiling with fine-grained control
    const context: IProfileContext = {
      functionName: 'processLargeDataset',
      tags: { dataSize: data.length.toString() },
      startTime: Date.now(),
    };

    this.pyroscope.StartProfiling(context);

    try {
      // Your processing logic
      const result = data.map(item => this.transform(item));
      return result;
    } finally {
      // Always stop profiling, even on error
      const metrics = this.pyroscope.StopProfiling(context);
      console.log(`Processing took ${metrics.duration}ms`);
    }
  }

  // Or use the convenience method
  async processWithTracking(data: any[]) {
    return await this.pyroscope.TrackFunction(
      'processWithTracking',
      async () => {
        // Your logic here
        return data.map(item => this.transform(item));
      },
      { dataSize: data.length.toString() }
    );
  }
}

Health Monitoring

Built-in Health Endpoints

The module automatically registers four health endpoints at /profiling/* (can be disabled):

GET /profiling/health

Returns health status with server connectivity and active profile information.

{
  "status": "healthy",
  "timestamp": 1710429254123,
  "uptime": 3600.5,
  "pyroscope": {
    "connected": true,
    "serverAddress": "http://localhost:4040",
    "applicationName": "my-app",
    "lastUpdate": 1710429254123
  },
  "profiling": {
    "enabled": true,
    "activeProfiles": 12,
    "totalProfiles": 5430
  }
}

GET /profiling/metrics

Returns aggregated profiling metrics (JSON format).

{
  "timestamp": 1710429254123,
  "cpu": {
    "samples": 1250,
    "duration": 45000
  },
  "memory": {
    "samples": 890,
    "allocations": 512000000
  },
  "requests": {
    "total": 5430,
    "successful": 5389,
    "failed": 41,
    "averageResponseTime": 125.34
  }
}

GET /profiling/status

Returns combined health and metrics in a single response.

GET /profiling/metrics/prometheus

Returns metrics in Prometheus exposition format for integration with Prometheus servers.

# HELP profiling_cpu_samples_total Total number of CPU profiling samples collected
# TYPE profiling_cpu_samples_total counter
profiling_cpu_samples_total 1250

# HELP profiling_requests_total Total number of requests profiled
# TYPE profiling_requests_total counter
profiling_requests_total 5430
...

ProfilingHealthIndicator - NestJS Health Check Integration

Integrate with NestJS health checks using @nestjs/terminus:

import { Controller, Get, Injectable } from '@nestjs/common';
import { HealthCheck, HealthCheckService } from '@nestjs/terminus';
import { ProfilingHealthIndicator } from '@pawells/nestjs-pyroscope';

@Controller('health')
@Injectable()
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private profiling: ProfilingHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.profiling.Check('profiling'),
    ]);
  }
}

Configuration Reference

IPyroscopeConfig Interface

All configuration uses plain TypeScript interfaces (no Joi validation).

interface IPyroscopeConfig {
  // Required fields
  enabled: boolean;                    // Enable/disable profiling
  serverAddress: string;               // Pyroscope server URL (http:// or https://)
  applicationName: string;             // Application identifier for Pyroscope

  // Optional: Metadata
  environment?: string;                // Environment name (dev, staging, prod)
  version?: string;                    // Application version
  sampleRate?: number;                 // Sampling rate (0-1)
  profileTypes?: TProfileType[];       // Profile types to collect

  // Optional: Tags
  tags?: Record<string, string>;       // Global tags applied to all profiles

  // Optional: Authentication
  basicAuthUser?: string;              // Basic auth username
  basicAuthPassword?: string;          // Basic auth password

  // Optional: TLS/SSL
  tlsEnabled?: boolean;                // Enable TLS
  tlsCertPath?: string;                // Path to TLS cert
  tlsKeyPath?: string;                 // Path to TLS key
  tlsCaPath?: string;                  // Path to CA cert
  tlsInsecureSkipVerify?: boolean;     // Skip TLS verification (unsafe)

  // Optional: Logging
  logLevel?: 'debug' | 'info' | 'warn' | 'error';  // Log level

  // Optional: Health and Monitoring
  enableHealthChecks?: boolean;        // Enable health endpoints (default: true)
  degradedActiveProfilesThreshold?: number;  // Threshold for degraded status (default: 1000)

  // Optional: Retry Configuration
  retryBaseDelayMs?: number;           // Base delay for exponential backoff (default: 100)
  retryMaxDelayMs?: number;            // Max delay for retries (default: 10000)
  retryJitterMs?: number;              // Jitter to add to delays (default: 1000)

  // Optional: Tag Handling
  tagMaxLength?: number;               // Max tag value length (default: 200)
}

Profile Types

type TProfileType = 'cpu' | 'memory' | 'goroutine' | 'mutex' | 'block';

IProfileMetrics Interface

Represents profiling metrics collected during a profiling session. Used to track performance data from individual operations:

interface IProfileMetrics {
  cpuTime: number;              // CPU time consumed in milliseconds
  memoryUsage: number;          // Memory usage in bytes
  duration: number;             // Total operation duration in milliseconds
  timestamp: number;            // Timestamp when metrics were recorded
  tags?: Record<string, string>; // Optional metadata tags for filtering/analysis
}

When to use:

  • You need to access metrics from a completed profiling session
  • You want to store or analyze profiling data programmatically
  • You're implementing custom metrics aggregation or reporting

Example:

import { Injectable } from '@nestjs/common';
import { PyroscopeService, IProfileMetrics } from '@pawells/nestjs-pyroscope';

@Injectable()
export class PerformanceAnalyzer {
  constructor(private readonly pyroscope: PyroscopeService) {}

  async trackAndLog(operation: string) {
    const context = {
      functionName: operation,
      startTime: Date.now(),
      tags: { operation },
    };

    this.pyroscope.StartProfiling(context);

    try {
      // Your logic here
      await this.expensiveOperation();
    } finally {
      const metrics = this.pyroscope.StopProfiling(context);
      console.log(`Operation: ${operation}`);
      console.log(`Duration: ${metrics.duration}ms`);
      console.log(`CPU: ${metrics.cpuTime}ms`);
      console.log(`Memory: ${metrics.memoryUsage} bytes`);
    }
  }
}

IProfileContext Interface

Represents the lifecycle and metadata of a profiling session. Used internally by PyroscopeService and provided by decorators/interceptors:

interface IProfileContext {
  profileId?: string;               // Unique profile identifier
  functionName: string;             // Name of the function/operation being profiled
  className?: string;               // Class name (for class methods)
  methodName?: string;              // Method name (for class methods)
  startTime?: number;               // Start timestamp in milliseconds
  endTime?: number;                 // End timestamp in milliseconds
  duration?: number;                // Computed duration in milliseconds
  error?: Error;                    // Error object if operation failed
  tags?: Record<string, string>;    // Metadata tags for profiling context
}

When to use:

  • You're implementing manual profiling with PyroscopeService.StartProfiling() and .StopProfiling()
  • You need to track profiling metadata for complex operations
  • You're building custom decorators or profiling utilities

Example:

import { Injectable } from '@nestjs/common';
import { PyroscopeService, IProfileContext } from '@pawells/nestjs-pyroscope';

@Injectable()
export class DataProcessingService {
  constructor(private readonly pyroscope: PyroscopeService) {}

  async processWithContext(data: any[]) {
    const context: IProfileContext = {
      functionName: 'processLargeDataset',
      className: 'DataProcessingService',
      methodName: 'processWithContext',
      startTime: Date.now(),
      tags: {
        dataSize: data.length.toString(),
        environment: process.env.NODE_ENV || 'development',
      },
    };

    this.pyroscope.StartProfiling(context);

    try {
      const result = data.map(item => this.transform(item));
      context.duration = Date.now() - (context.startTime || 0);
      return result;
    } catch (error) {
      context.error = error as Error;
      context.duration = Date.now() - (context.startTime || 0);
      throw error;
    } finally {
      this.pyroscope.StopProfiling(context);
    }
  }
}

MetricsService API

The MetricsService is injected with the module and provides metrics aggregation:

import { Injectable } from '@nestjs/common';
import { MetricsService } from '@pawells/nestjs-pyroscope';

@Injectable()
export class MetricsController {
  constructor(private readonly metrics: MetricsService) {}

  getMetrics() {
    // Get aggregated metrics snapshot
    const snapshot = this.metrics.GetMetrics();

    // Record individual metrics
    this.metrics.RecordCPUSample(125);  // CPU time in ms
    this.metrics.RecordMemorySample(1024000);  // Memory in bytes
    this.metrics.RecordRequest(200, 45);  // Status code and duration

    // Export Prometheus format
    const prometheus = this.metrics.GetPrometheusMetrics();

    // Reset all metrics
    this.metrics.Reset();
  }
}

Utility Classes

ProfilingConfigValidator

import { ProfilingConfigValidator } from '@pawells/nestjs-pyroscope';

const result = ProfilingConfigValidator.Validate(config);
if (!result.isValid) {
  console.error('Config errors:', result.errors);
}

TagFormatter

import { TagFormatter } from '@pawells/nestjs-pyroscope';

// Convert camelCase to snake_case
TagFormatter.Format({ userId: '123', userName: 'john' });
// Returns: { user_id: '123', user_name: 'john' }

// Merge tags
TagFormatter.Merge({ env: 'prod' }, { region: 'us-east-1' });
// Returns: { env: 'prod', region: 'us-east-1' }

// Sanitize tags
TagFormatter.Sanitize({ valid: 'value', empty: '', token: 'verylongvalue' });
// Returns: { valid: 'value', token: 'very' } (long value truncated)

MetricAggregator

import { MetricAggregator } from '@pawells/nestjs-pyroscope';

const metrics = [
  { duration: 100 },
  { duration: 200 },
  { duration: 150 },
];

// Calculate statistics
const avg = MetricAggregator.AverageDuration(metrics);  // 150
const p95 = MetricAggregator.Percentile(metrics, 95);   // ~200

// Group by tags
const grouped = MetricAggregator.GroupByTags(
  metricsWithTags,
  ['operation']
);

ProfilingErrorHandler

import { ProfilingErrorHandler } from '@pawells/nestjs-pyroscope';

try {
  // Operation
} catch (error) {
  if (ProfilingErrorHandler.IsRecoverableError(error)) {
    // Schedule retry
    const delay = ProfilingErrorHandler.GetRetryDelay(error, attempt);
    setTimeout(() => retry(), delay);
  }

  // Format error safely for logging
  const message = ProfilingErrorHandler.FormatError(error);
  logger.error(message);
}

Utility Functions

import {
  GenerateProfileId,
  FormatDuration,
  IsProfilingEnabled,
} from '@pawells/nestjs-pyroscope';

// Generate unique profile ID
const id = GenerateProfileId('operation');
// Returns: 'operation_1710429254123_abc123def'

// Format duration
FormatDuration(450);   // '450.00ms'
FormatDuration(1500);  // '1.50s'

// Check if enabled via env
if (IsProfilingEnabled()) {
  // PYROSCOPE_ENABLED=true or PYROSCOPE_ENABLED=1
}

Best Practices

1. Selective Profiling

Profile only critical paths and hot functions to minimize overhead:

@Injectable()
export class UserService {
  @ProfileMethod({ name: 'user.expensive-operation' })
  async expensiveOperation() {
    // Only profile what matters
  }

  // Don't profile trivial operations
  getName() {
    return this.name;
  }
}

2. Use Descriptive Names

Provide meaningful profile names for better analysis in Pyroscope UI:

@ProfileMethod({ name: 'payment.processing.stripe' })
async processPayment(amount: number) {
  // Clear, specific name for easy tracking
}

3. Add Contextual Tags

Include relevant context in tags for filtering and analysis:

@ProfileMethod({ tags: { 'query-type': 'complex', 'table': 'orders' } })
async executeComplexQuery() {
  // Tags help correlate performance with operation type
}

4. Environment-based Configuration

Configure differently per environment:

PyroscopeModule.ForRootAsync({
  useFactory: (configService: ConfigService) => ({
    enabled: configService.get('NODE_ENV') !== 'test',
    serverAddress: configService.get('PYROSCOPE_SERVER'),
    applicationName: configService.get('APP_NAME'),
    environment: configService.get('NODE_ENV'),
    tags: {
      region: configService.get('REGION'),
      version: configService.get('APP_VERSION'),
      service: configService.get('SERVICE_NAME'),
    },
  }),
  inject: [ConfigService],
})

5. Disable Health Endpoints in Production

If you want to disable the built-in health endpoints:

PyroscopeModule.ForRoot({
  config: { /* ... */ },
  enableHealthChecks: false,
})

6. Secure Health Endpoints

⚠️ WARNING: The health endpoints expose infrastructure and performance information. Protect them at the network level:

// Example: Protect with middleware
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';

@Module({})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(authMiddleware)
      .forRoutes('profiling');
  }
}

Integration with Other Packages

nestjs-pyroscope depends on @pawells/nestjs-shared and works with the rest of the @pawells/nestjs-* suite:

Implementation Notes

Fire-and-Forget Initialization

Pyroscope client initialization happens asynchronously in the background to avoid blocking module startup. If initialization fails, profiling gracefully degrades and the application continues normally.

Memory Management

The service automatically manages memory to prevent unbounded growth:

  • Max Active Profiles: 10,000 concurrent profiles (older ones evicted)
  • Stale Profile Timeout: 30 minutes (profiles older than this are evicted)
  • Metrics History: 1,000 entries (oldest entries discarded)

Dynamic Tags Not Supported

@pyroscope/nodejs does not support dynamic tag manipulation. Tags must be set during module initialization via config.tags. Context tags are tracked for metrics but don't affect Pyroscope server-side profiling.

License

MIT