OpenTelemetry Sampling for Node: Stay Useful at Fifty Thousand RPS

OpenTelemetry Sampling for Node: Stay Useful at Fifty Thousand RPS
8 views
At 50k RPS, 100% traces are not observability — they are a denial-of-wallet attack against your collector. Head sampling alone drops errors you needed; no sampling melts X-Ray/OTLP exporters and Node event loops under protobuf encode. The unfair advantage is layered policy: low head sample for success, guaranteed keep for errors/high latency via tail sampling (or parent-based rules), and exporter backpressure that sheds before the API does.
⚡ TL;DR: Start with parent-based head sampler at 1–5% for healthy traffic; force-sample error and >p99 latency traces; run a tail sampler (Gateway/collector) for final decisions; bound queue + batch in the Node SDK. Pair with OpenTelemetry Tail Sampling and OpenTelemetry for LLMs.

OpenTelemetry Sampling for Node: production guidance

typescript
// src/otel.ts
import { NodeSDK } from "@opentelemetry/sdk-node";
import { ParentBasedSampler, TraceIdRatioBasedSampler, SamplingDecision } from "@opentelemetry/sdk-trace-base";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";

class ErrorAwareRatioSampler extends TraceIdRatioBasedSampler {
 // Ratio applied only when parent says "ask child"
 constructor(private ratio: number) { super(ratio); }
}

const exporter = new OTLPTraceExporter({ url: process.env.OTLP_URL });
const processor = new BatchSpanProcessor(exporter, {
 maxQueueSize: 2048, // ✅ bound memory
 scheduledDelayMillis: 5000,
 maxExportBatchSize: 512,
});

const sdk = new NodeSDK({
 sampler: new ParentBasedSampler({
 root: new TraceIdRatioBasedSampler(Number(process.env.TRACE_RATIO ?? "0.02")),
 }),
 spanProcessors: [processor],
});
sdk.start();
typescript
// ❌ AlwaysOnSampler in production at 50k RPS
import { AlwaysOnSampler } from "@opentelemetry/sdk-trace-base";

Force-keep errors at the edges

typescript
// src/http-hook.ts — after response, bump sampling bit for bad outcomes
import { trace, SpanStatusCode } from "@opentelemetry/api";

export function finalizeSpan(status: number, durationMs: number) {
 const span = trace.getActiveSpan();
 if (!span) return;
 if (status >= 500 || durationMs > 2000) {
 span.setStatus({ code: SpanStatusCode.ERROR });
 span.setAttribute("sampling.force", true);
 // Tail sampler / collector rule keys off this attribute or status
 }
}
Collector sketch (tail):
yaml
# otel-collector-config.yaml (excerpt)
processors:
 tail_sampling:
 decision_wait: 10s
 policies:
 - name: errors
 type: status_code
 status_code: { status_codes: [ERROR] }
 - name: slow
 type: latency
 latency: { threshold_ms: 2000 }
 - name: probabilistic
 type: probabilistic
 probabilistic: { sampling_percentage: 2 }

Budget math at 50k RPS

Assumption Value
RPS 50,000
2% head sample 1,000 root traces/s
Spans/trace ~8
Export ~8k spans/s — still heavy; push ratio to 0.5–1% + tail keep errors
Node risk Batch encode on event loop → watch exporter drop metrics

Closing checklist

✅ Dos – ✅ Parent-based ratio for roots; respect upstream sample flags – ✅ Tail-keep errors and slow requests – ✅ Bound BatchSpanProcessor queues; monitor drops – ✅ Separate trace pipeline SLOs from app SLOs – ✅ Load-test collectors at peak, not average ❌ Don’ts – ❌ Don’t run AlwaysOn in prod “just for a week” – ❌ Don’t sample only in the app with no collector policy – ❌ Don’t ignore exporter error/drop metrics – ❌ Don’t attach huge payloads as span attributes

Related reading

Last updated on September 11, 2026 Related: Python OpenTelemetry Spans: Useful Traces Without Decorator Soup Everywhere; Node.js Event Loop Blocking: The Silent API Killer and How to Fix It; Node.js Streams Backpressure: The Memory Leak That Kills Servers at 3AM.

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.