OpenTelemetry JavaScript Sampling

What is sampling?

Sampling is a process that restricts the amount of spans that are generated by a system. In high-volume applications, collecting 100% of traces can be expensive and unnecessary. Sampling allows you to collect a representative subset of traces while reducing costs and performance overhead.

JavaScript sampling overview

OpenTelemetry JavaScript SDK provides head-based sampling capabilities where the sampling decision is made at the beginning of a trace. By default, the tracer provider uses a ParentBased sampler with the AlwaysOn sampler. You can configure a sampler on the tracer provider using the sampler option.

Built-in samplers

The JavaScript SDK provides several built-in samplers for common use cases.

AlwaysOnSampler

Samples every trace. Useful for development environments but use with caution in production with significant traffic:

js Node.js
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { AlwaysOnSampler } = require('@opentelemetry/sdk-trace-base');

const provider = new NodeTracerProvider({
  sampler: new AlwaysOnSampler(),
});

provider.register();
js Browser
const { WebTracerProvider } = require('@opentelemetry/sdk-trace-web');
const { AlwaysOnSampler } = require('@opentelemetry/sdk-trace-base');

const provider = new WebTracerProvider({
  sampler: new AlwaysOnSampler(),
});

provider.register();

AlwaysOffSampler

Samples no traces. Useful for completely disabling tracing:

js Node.js
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { AlwaysOffSampler } = require('@opentelemetry/sdk-trace-base');

const provider = new NodeTracerProvider({
  sampler: new AlwaysOffSampler(),
});

provider.register();
js Browser
const { WebTracerProvider } = require('@opentelemetry/sdk-trace-web');
const { AlwaysOffSampler } = require('@opentelemetry/sdk-trace-base');

const provider = new WebTracerProvider({
  sampler: new AlwaysOffSampler(),
});

provider.register();

TraceIdRatioBasedSampler

Samples a fraction of traces based on the trace ID. The fraction should be between 0.0 and 1.0:

js Node.js
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { TraceIdRatioBasedSampler } = require('@opentelemetry/sdk-trace-base');

// Sample 10% of traces
const provider = new NodeTracerProvider({
  sampler: new TraceIdRatioBasedSampler(0.1),
});

provider.register();
js Browser
const { WebTracerProvider } = require('@opentelemetry/sdk-trace-web');
const { TraceIdRatioBasedSampler } = require('@opentelemetry/sdk-trace-base');

// Sample 10% of traces
const provider = new WebTracerProvider({
  sampler: new TraceIdRatioBasedSampler(0.1),
});

provider.register();

ParentBasedSampler

A sampler decorator that behaves differently based on the parent of the span. If the span has no parent, the root sampler is used to make the sampling decision. If the span has a parent, the sampler follows the parent's sampling decision:

js Node.js
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const {
  ParentBasedSampler,
  TraceIdRatioBasedSampler,
  AlwaysOnSampler,
  AlwaysOffSampler,
} = require('@opentelemetry/sdk-trace-base');

// ParentBased with TraceIdRatioBased root sampler
const provider = new NodeTracerProvider({
  sampler: new ParentBasedSampler({
    root: new TraceIdRatioBasedSampler(0.1),
  }),
});

provider.register();
js Browser
const { WebTracerProvider } = require('@opentelemetry/sdk-trace-web');
const {
  ParentBasedSampler,
  TraceIdRatioBasedSampler,
} = require('@opentelemetry/sdk-trace-base');

// ParentBased with TraceIdRatioBased root sampler
const provider = new WebTracerProvider({
  sampler: new ParentBasedSampler({
    root: new TraceIdRatioBasedSampler(0.1),
  }),
});

provider.register();

The ParentBasedSampler accepts additional configuration options:

js
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const {
  ParentBasedSampler,
  TraceIdRatioBasedSampler,
  AlwaysOnSampler,
  AlwaysOffSampler,
} = require('@opentelemetry/sdk-trace-base');

const provider = new NodeTracerProvider({
  sampler: new ParentBasedSampler({
    // Sampler for root spans (no parent)
    root: new TraceIdRatioBasedSampler(0.5),
    // Sampler when remote parent is sampled
    remoteParentSampled: new AlwaysOnSampler(),
    // Sampler when remote parent is not sampled
    remoteParentNotSampled: new AlwaysOffSampler(),
    // Sampler when local parent is sampled
    localParentSampled: new AlwaysOnSampler(),
    // Sampler when local parent is not sampled
    localParentNotSampled: new AlwaysOffSampler(),
  }),
});

provider.register();

Configuration options

Environment variables

You can configure sampling using environment variables:

bash
# TraceIdRatioBased sampler with 10% sampling
export OTEL_TRACES_SAMPLER="traceidratio"
export OTEL_TRACES_SAMPLER_ARG="0.1"

# ParentBased with TraceIdRatioBased
export OTEL_TRACES_SAMPLER="parentbased_traceidratio"
export OTEL_TRACES_SAMPLER_ARG="0.1"

# Always sample
export OTEL_TRACES_SAMPLER="always_on"

# Never sample
export OTEL_TRACES_SAMPLER="always_off"

# ParentBased with AlwaysOn (default)
export OTEL_TRACES_SAMPLER="parentbased_always_on"

Programmatic configuration

js
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { resourceFromAttributes } = require('@opentelemetry/resources');
const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } = require('@opentelemetry/semantic-conventions');
const {
  ParentBasedSampler,
  TraceIdRatioBasedSampler,
  AlwaysOnSampler,
} = require('@opentelemetry/sdk-trace-base');

function setupTracing() {
  // Create OTLP exporter
  const exporter = new OTLPTraceExporter({
    url: 'https://api.uptrace.dev/v1/traces',
    headers: {
      'uptrace-dsn': process.env.UPTRACE_DSN,
    },
  });

  // Configure sampler based on environment
  let sampler;
  const env = process.env.NODE_ENV;

  switch (env) {
    case 'development':
      sampler = new AlwaysOnSampler();
      break;
    case 'production':
      // 10% sampling for production
      sampler = new ParentBasedSampler({
        root: new TraceIdRatioBasedSampler(0.1),
      });
      break;
    default:
      // 25% sampling for staging/test
      sampler = new ParentBasedSampler({
        root: new TraceIdRatioBasedSampler(0.25),
      });
  }

  // Create tracer provider
  const provider = new NodeTracerProvider({
    resource: resourceFromAttributes({
      [ATTR_SERVICE_NAME]: 'my-service',
      [ATTR_SERVICE_VERSION]: '1.0.0',
    }),
    sampler: sampler,
    spanProcessors: [new BatchSpanProcessor(exporter)],
  });

  provider.register();

  return () => provider.shutdown();
}

module.exports = { setupTracing };

Custom samplers

Create custom sampling logic by implementing the Sampler interface:

js
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { SamplingDecision } = require('@opentelemetry/api');

// Custom sampler that samples based on span attributes
class PathBasedSampler {
  shouldSample(context, traceId, spanName, spanKind, attributes, links) {
    const httpTarget = attributes['http.target'] || attributes['url.path'];

    // Always sample API endpoints
    if (httpTarget && httpTarget.startsWith('/api/')) {
      return {
        decision: SamplingDecision.RECORD_AND_SAMPLED,
      };
    }

    // Never sample health check endpoints
    if (httpTarget === '/health' || httpTarget === '/healthz' || httpTarget === '/ready') {
      return {
        decision: SamplingDecision.NOT_RECORD,
      };
    }

    // Sample 10% of other requests
    const threshold = 0.1;
    const random = Math.random();
    return {
      decision:
        random < threshold
          ? SamplingDecision.RECORD_AND_SAMPLED
          : SamplingDecision.NOT_RECORD,
    };
  }

  toString() {
    return 'PathBasedSampler';
  }
}

const provider = new NodeTracerProvider({
  sampler: new PathBasedSampler(),
});

provider.register();

Attribute-based sampling

js
const { SamplingDecision } = require('@opentelemetry/api');
const { TraceIdRatioBasedSampler, AlwaysOnSampler } = require('@opentelemetry/sdk-trace-base');

class AttributeBasedSampler {
  constructor() {
    this.highPrioritySampler = new AlwaysOnSampler();
    this.defaultSampler = new TraceIdRatioBasedSampler(0.05); // 5% default
  }

  shouldSample(context, traceId, spanName, spanKind, attributes, links) {
    // Always sample spans with errors
    if (attributes['error'] === true) {
      return this.highPrioritySampler.shouldSample(
        context,
        traceId,
        spanName,
        spanKind,
        attributes,
        links
      );
    }

    // Always sample critical operations
    if (
      spanName.includes('critical') ||
      spanName.includes('payment') ||
      spanName.includes('auth')
    ) {
      return this.highPrioritySampler.shouldSample(
        context,
        traceId,
        spanName,
        spanKind,
        attributes,
        links
      );
    }

    // Check for high priority attributes
    const httpRoute = attributes['http.route'];
    if (httpRoute) {
      // Always sample admin routes
      if (httpRoute.startsWith('/admin')) {
        return this.highPrioritySampler.shouldSample(
          context,
          traceId,
          spanName,
          spanKind,
          attributes,
          links
        );
      }
    }

    // Check user tier
    const userTier = attributes['user.tier'];
    if (userTier === 'premium') {
      return this.highPrioritySampler.shouldSample(
        context,
        traceId,
        spanName,
        spanKind,
        attributes,
        links
      );
    }

    // Check HTTP status code for errors
    const statusCode = attributes['http.status_code'] || attributes['http.response.status_code'];
    if (statusCode && statusCode >= 400) {
      return this.highPrioritySampler.shouldSample(
        context,
        traceId,
        spanName,
        spanKind,
        attributes,
        links
      );
    }

    // Use default sampler for other cases
    return this.defaultSampler.shouldSample(
      context,
      traceId,
      spanName,
      spanKind,
      attributes,
      links
    );
  }

  toString() {
    return 'AttributeBasedSampler{admin=always,premium=always,errors=always,default=5%}';
  }
}

Rate-limiting sampler

js
const { SamplingDecision } = require('@opentelemetry/api');

class RateLimitingSampler {
  constructor(maxSamplesPerSecond) {
    this.maxSamplesPerSecond = maxSamplesPerSecond;
    this.lastSecond = 0;
    this.currentCount = 0;
  }

  shouldSample(context, traceId, spanName, spanKind, attributes, links) {
    const now = Math.floor(Date.now() / 1000);

    if (now !== this.lastSecond) {
      this.lastSecond = now;
      this.currentCount = 0;
    }

    if (this.currentCount < this.maxSamplesPerSecond) {
      this.currentCount++;
      return {
        decision: SamplingDecision.RECORD_AND_SAMPLED,
      };
    }

    return {
      decision: SamplingDecision.NOT_RECORD,
    };
  }

  toString() {
    return `RateLimitingSampler{${this.maxSamplesPerSecond} samples/second}`;
  }
}

// Usage: limit to 100 samples per second
const provider = new NodeTracerProvider({
  sampler: new RateLimitingSampler(100),
});

Production deployment examples

Express.js server with sampling

js
const express = require('express');
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { resourceFromAttributes } = require('@opentelemetry/resources');
const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, ATTR_DEPLOYMENT_ENVIRONMENT } = require('@opentelemetry/semantic-conventions');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
const { trace } = require('@opentelemetry/api');
const {
  ParentBasedSampler,
  TraceIdRatioBasedSampler,
  AlwaysOnSampler,
} = require('@opentelemetry/sdk-trace-base');

// Setup tracing before importing other modules
function setupTracing() {
  const exporter = new OTLPTraceExporter({
    url: 'https://api.uptrace.dev/v1/traces',
    headers: {
      'uptrace-dsn': process.env.UPTRACE_DSN,
    },
  });

  // Configure sampling based on environment
  let sampler;
  switch (process.env.NODE_ENV) {
    case 'development':
      sampler = new AlwaysOnSampler();
      break;
    case 'production':
      sampler = new ParentBasedSampler({
        root: new TraceIdRatioBasedSampler(0.1), // 10% sampling
      });
      break;
    default:
      sampler = new ParentBasedSampler({
        root: new TraceIdRatioBasedSampler(0.25), // 25% sampling
      });
  }

  const provider = new NodeTracerProvider({
    resource: resourceFromAttributes({
      [ATTR_SERVICE_NAME]: 'my-express-server',
      [ATTR_SERVICE_VERSION]: '1.0.0',
      [ATTR_DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development',
    }),
    sampler: sampler,
    spanProcessors: [
      new BatchSpanProcessor(exporter, {
        maxQueueSize: 2048,
        maxExportBatchSize: 512,
        scheduledDelayMillis: 5000,
        exportTimeoutMillis: 30000,
      }),
    ],
  });

  provider.register();

  registerInstrumentations({
    instrumentations: [new HttpInstrumentation(), new ExpressInstrumentation()],
  });

  return () => provider.shutdown();
}

const shutdown = setupTracing();

// Create Express app
const app = express();
const tracer = trace.getTracer('my-express-server');

app.get('/api/users/:id', async (req, res) => {
  const span = trace.getActiveSpan();

  // Add attributes only if span is recording
  if (span && span.isRecording()) {
    span.setAttribute('user.id', req.params.id);
    span.setAttribute('http.route', '/api/users/:id');
  }

  // Simulate database call
  const user = await getUserById(req.params.id);

  res.json(user);
});

app.get('/health', (req, res) => {
  res.json({ status: 'healthy' });
});

async function getUserById(id) {
  return tracer.startActiveSpan('getUserById', async (span) => {
    try {
      // Simulate database query
      await new Promise((resolve) => setTimeout(resolve, 50));

      if (span.isRecording()) {
        span.setAttribute('db.system', 'postgresql');
        span.setAttribute('db.operation', 'SELECT');
      }

      return { id, name: 'John Doe', email: 'john@example.com' };
    } finally {
      span.end();
    }
  });
}

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  await shutdown();
  process.exit(0);
});

Browser application with sampling

js
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
import {
  ParentBasedSampler,
  TraceIdRatioBasedSampler,
} from '@opentelemetry/sdk-trace-base';

function setupBrowserTracing() {
  const exporter = new OTLPTraceExporter({
    url: 'https://api.uptrace.dev/v1/traces',
    headers: {
      'uptrace-dsn': 'YOUR_UPTRACE_DSN',
    },
  });

  // Use lower sampling rate for browser to reduce data volume
  const sampler = new ParentBasedSampler({
    root: new TraceIdRatioBasedSampler(0.05), // 5% sampling for browser
  });

  const provider = new WebTracerProvider({
    resource: resourceFromAttributes({
      [ATTR_SERVICE_NAME]: 'my-browser-app',
      [ATTR_SERVICE_VERSION]: '1.0.0',
    }),
    sampler: sampler,
    spanProcessors: [
      new BatchSpanProcessor(exporter, {
        maxQueueSize: 100,
        maxExportBatchSize: 10,
        scheduledDelayMillis: 500,
        exportTimeoutMillis: 30000,
      }),
    ],
  });

  provider.register({
    contextManager: new ZoneContextManager(),
  });

  registerInstrumentations({
    instrumentations: [
      new FetchInstrumentation({
        propagateTraceHeaderCorsUrls: [/your-api-domain\.com/],
        clearTimingResources: true,
      }),
      new XMLHttpRequestInstrumentation({
        propagateTraceHeaderCorsUrls: [/your-api-domain\.com/],
      }),
    ],
  });
}

setupBrowserTracing();

Composite samplers (experimental)

The OpenTelemetry JavaScript SDK provides experimental composite samplers for more complex sampling scenarios:

js
const {
  createCompositeSampler,
  createComposableAlwaysOffSampler,
  createComposableAlwaysOnSampler,
  createComposableParentThresholdSampler,
  createComposableTraceIDRatioBasedSampler,
} = require('@opentelemetry/sampler-composite');

// Never sample
const neverSampler = createCompositeSampler(createComposableAlwaysOffSampler());

// Always sample
const alwaysSampler = createCompositeSampler(createComposableAlwaysOnSampler());

// Follow the parent, or sample with 30% probability for root spans
const parentBasedSampler = createCompositeSampler(
  createComposableParentThresholdSampler(createComposableTraceIDRatioBasedSampler(0.3))
);

Best practices

Performance optimization

When implementing sampling, consider these performance best practices:

  1. Check span recording status: Use span.isRecording() before adding expensive attributes or operations
  2. Use batch processing: Always use BatchSpanProcessor instead of SimpleSpanProcessor in production
  3. Configure appropriate batch sizes: Balance memory usage and export frequency based on your traffic patterns
  4. Avoid expensive operations in samplers: Keep shouldSample implementations fast and simple

Production considerations

For production deployments:

  1. Start conservative: Begin with lower sampling rates (1-5%) and increase gradually based on your observability needs
  2. Use ParentBasedSampler: Ensures consistent sampling decisions across service boundaries
  3. Monitor sampling effectiveness: Track sampling rates and ensure you're capturing enough data for meaningful analysis
  4. Consider costs: Balance observability value with storage and processing costs
  5. Use environment-based configuration: Configure different sampling rates for development, staging, and production
  6. Implement fallback strategies: Ensure your application continues to function even if sampling configuration fails

Checking if a span is recording

To avoid unnecessary work when a span is not being sampled:

js
const { trace } = require('@opentelemetry/api');

function processRequest(req) {
  const span = trace.getActiveSpan();

  // Only add expensive attributes if the span is recording
  if (span && span.isRecording()) {
    span.setAttribute('request.body.size', JSON.stringify(req.body).length);
    span.setAttribute('request.headers', JSON.stringify(req.headers));
  }

  // Process the request...
}

What's next?